fix(ollama): preserve reasoning and tool-call context (#6605)
This commit is contained in:
@@ -5,12 +5,13 @@ import (
|
||||
)
|
||||
|
||||
type OllamaChatMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
ToolCalls []OllamaToolCall `json:"tool_calls,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
Thinking json.RawMessage `json:"thinking,omitempty"`
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
ToolCalls []OllamaToolCall `json:"tool_calls,omitempty"`
|
||||
ToolName string `json:"tool_name,omitempty"`
|
||||
ToolCallID string `json:"tool_call_id,omitempty"`
|
||||
Thinking json.RawMessage `json:"thinking,omitempty"`
|
||||
}
|
||||
|
||||
type OllamaToolFunction struct {
|
||||
@@ -25,6 +26,7 @@ type OllamaTool struct {
|
||||
}
|
||||
|
||||
type OllamaToolCall struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Function struct {
|
||||
Name string `json:"name"`
|
||||
Arguments interface{} `json:"arguments"`
|
||||
@@ -36,7 +38,7 @@ type OllamaChatRequest struct {
|
||||
Messages []OllamaChatMessage `json:"messages"`
|
||||
Tools interface{} `json:"tools,omitempty"`
|
||||
Format interface{} `json:"format,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
KeepAlive interface{} `json:"keep_alive,omitempty"`
|
||||
Think json.RawMessage `json:"think,omitempty"`
|
||||
@@ -48,7 +50,7 @@ type OllamaGenerateRequest struct {
|
||||
Suffix string `json:"suffix,omitempty"`
|
||||
Images []string `json:"images,omitempty"`
|
||||
Format interface{} `json:"format,omitempty"`
|
||||
Stream bool `json:"stream,omitempty"`
|
||||
Stream bool `json:"stream"`
|
||||
Options map[string]any `json:"options,omitempty"`
|
||||
KeepAlive interface{} `json:"keep_alive,omitempty"`
|
||||
Think json.RawMessage `json:"think,omitempty"`
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ollama
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -19,24 +18,67 @@ import (
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func toOllamaResponseFormat(responseFormat *dto.ResponseFormat) (any, error) {
|
||||
if responseFormat == nil {
|
||||
return nil, nil
|
||||
}
|
||||
switch responseFormat.Type {
|
||||
case "json", "json_object":
|
||||
return "json", nil
|
||||
case "json_schema":
|
||||
if len(responseFormat.JsonSchema) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var jsonSchema dto.FormatJsonSchema
|
||||
if err := common.Unmarshal(responseFormat.JsonSchema, &jsonSchema); err != nil {
|
||||
return nil, fmt.Errorf("invalid ollama response format: %w", err)
|
||||
}
|
||||
return jsonSchema.Schema, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaChatRequest, error) {
|
||||
think := r.Think
|
||||
if len(think) == 0 {
|
||||
effort := r.ReasoningEffort
|
||||
if len(r.Reasoning) > 0 {
|
||||
var reasoning dto.Reasoning
|
||||
if err := common.Unmarshal(r.Reasoning, &reasoning); err != nil {
|
||||
return nil, fmt.Errorf("invalid ollama reasoning: %w", err)
|
||||
}
|
||||
effort = lo.CoalesceOrEmpty(reasoning.Effort, effort)
|
||||
}
|
||||
if effort != "" {
|
||||
var thinkValue any
|
||||
switch effort {
|
||||
case "none":
|
||||
thinkValue = false
|
||||
case "low", "medium", "high", "max":
|
||||
thinkValue = effort
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported ollama reasoning effort %q", effort)
|
||||
}
|
||||
var err error
|
||||
think, err = common.Marshal(thinkValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal ollama think: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatReq := &OllamaChatRequest{
|
||||
Model: r.Model,
|
||||
Stream: lo.FromPtrOr(r.Stream, false),
|
||||
Options: map[string]any{},
|
||||
Think: r.Think,
|
||||
Think: think,
|
||||
}
|
||||
if r.ResponseFormat != nil {
|
||||
if r.ResponseFormat.Type == "json" {
|
||||
chatReq.Format = "json"
|
||||
} else if r.ResponseFormat.Type == "json_schema" {
|
||||
if len(r.ResponseFormat.JsonSchema) > 0 {
|
||||
var schema any
|
||||
_ = json.Unmarshal(r.ResponseFormat.JsonSchema, &schema)
|
||||
chatReq.Format = schema
|
||||
}
|
||||
}
|
||||
format, err := toOllamaResponseFormat(r.ResponseFormat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
chatReq.Format = format
|
||||
|
||||
// options mapping
|
||||
if r.Temperature != nil {
|
||||
@@ -68,12 +110,10 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam
|
||||
case []string:
|
||||
chatReq.Options["stop"] = v
|
||||
case []any:
|
||||
arr := make([]string, 0, len(v))
|
||||
for _, i := range v {
|
||||
if s, ok := i.(string); ok {
|
||||
arr = append(arr, s)
|
||||
}
|
||||
}
|
||||
arr := lo.FilterMap(v, func(item any, _ int) (string, bool) {
|
||||
value, ok := item.(string)
|
||||
return value, ok
|
||||
})
|
||||
if len(arr) > 0 {
|
||||
chatReq.Options["stop"] = arr
|
||||
}
|
||||
@@ -81,14 +121,20 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam
|
||||
}
|
||||
|
||||
if len(r.Tools) > 0 {
|
||||
tools := make([]OllamaTool, 0, len(r.Tools))
|
||||
for _, t := range r.Tools {
|
||||
tools = append(tools, OllamaTool{Type: "function", Function: OllamaToolFunction{Name: t.Function.Name, Description: t.Function.Description, Parameters: t.Function.Parameters}})
|
||||
}
|
||||
chatReq.Tools = tools
|
||||
chatReq.Tools = lo.Map(r.Tools, func(tool dto.ToolCallRequest, _ int) OllamaTool {
|
||||
return OllamaTool{
|
||||
Type: "function",
|
||||
Function: OllamaToolFunction{
|
||||
Name: tool.Function.Name,
|
||||
Description: tool.Function.Description,
|
||||
Parameters: tool.Function.Parameters,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
chatReq.Messages = make([]OllamaChatMessage, 0, len(r.Messages))
|
||||
toolNamesByCallID := make(map[string]string)
|
||||
for _, m := range r.Messages {
|
||||
var textBuilder strings.Builder
|
||||
var images []string
|
||||
@@ -117,8 +163,18 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam
|
||||
if len(images) > 0 {
|
||||
cm.Images = images
|
||||
}
|
||||
if m.Role == "tool" && m.Name != nil {
|
||||
cm.ToolName = *m.Name
|
||||
if m.Role == "assistant" {
|
||||
if reasoning, ok := lo.Coalesce(m.ReasoningContent, m.Reasoning); ok {
|
||||
thinking, err := common.Marshal(*reasoning)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal ollama thinking: %w", err)
|
||||
}
|
||||
cm.Thinking = thinking
|
||||
}
|
||||
}
|
||||
if m.Role == "tool" {
|
||||
cm.ToolCallID = m.ToolCallId
|
||||
cm.ToolName = lo.CoalesceOrEmpty(lo.FromPtr(m.Name), toolNamesByCallID[m.ToolCallId])
|
||||
}
|
||||
if m.ToolCalls != nil && len(m.ToolCalls) > 0 {
|
||||
parsed := m.ParseToolCalls()
|
||||
@@ -127,15 +183,18 @@ func openAIChatToOllamaChat(c *gin.Context, r *dto.GeneralOpenAIRequest) (*Ollam
|
||||
for _, tc := range parsed {
|
||||
var args interface{}
|
||||
if tc.Function.Arguments != "" {
|
||||
_ = json.Unmarshal([]byte(tc.Function.Arguments), &args)
|
||||
_ = common.Unmarshal([]byte(tc.Function.Arguments), &args)
|
||||
}
|
||||
if args == nil {
|
||||
args = map[string]any{}
|
||||
}
|
||||
oc := OllamaToolCall{}
|
||||
oc := OllamaToolCall{ID: tc.ID}
|
||||
oc.Function.Name = tc.Function.Name
|
||||
oc.Function.Arguments = args
|
||||
calls = append(calls, oc)
|
||||
if tc.ID != "" {
|
||||
toolNamesByCallID[tc.ID] = tc.Function.Name
|
||||
}
|
||||
}
|
||||
cm.ToolCalls = calls
|
||||
}
|
||||
@@ -175,15 +234,11 @@ func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGener
|
||||
gen.Suffix = s
|
||||
}
|
||||
}
|
||||
if r.ResponseFormat != nil {
|
||||
if r.ResponseFormat.Type == "json" {
|
||||
gen.Format = "json"
|
||||
} else if r.ResponseFormat.Type == "json_schema" {
|
||||
var schema any
|
||||
_ = json.Unmarshal(r.ResponseFormat.JsonSchema, &schema)
|
||||
gen.Format = schema
|
||||
}
|
||||
format, err := toOllamaResponseFormat(r.ResponseFormat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gen.Format = format
|
||||
if r.Temperature != nil {
|
||||
gen.Options["temperature"] = r.Temperature
|
||||
}
|
||||
@@ -212,12 +267,10 @@ func openAIToGenerate(c *gin.Context, r *dto.GeneralOpenAIRequest) (*OllamaGener
|
||||
case []string:
|
||||
gen.Options["stop"] = v
|
||||
case []any:
|
||||
arr := make([]string, 0, len(v))
|
||||
for _, i := range v {
|
||||
if s, ok := i.(string); ok {
|
||||
arr = append(arr, s)
|
||||
}
|
||||
}
|
||||
arr := lo.FilterMap(v, func(item any, _ int) (string, bool) {
|
||||
value, ok := item.(string)
|
||||
return value, ok
|
||||
})
|
||||
if len(arr) > 0 {
|
||||
gen.Options["stop"] = arr
|
||||
}
|
||||
@@ -510,7 +563,7 @@ func FetchOllamaVersion(baseURL, apiKey string) (string, error) {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &versionResp); err != nil {
|
||||
if err := common.Unmarshal(body, &versionResp); err != nil {
|
||||
return "", fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -58,8 +58,12 @@ func ollamaToolCallsToOpenAI(toolCalls []OllamaToolCall, startIndex int, include
|
||||
argBytes = []byte("{}")
|
||||
}
|
||||
}
|
||||
toolCallID := tc.ID
|
||||
if toolCallID == "" {
|
||||
toolCallID = fmt.Sprintf("call_%d", startIndex)
|
||||
}
|
||||
tr := dto.ToolCallResponse{
|
||||
ID: fmt.Sprintf("call_%d", startIndex),
|
||||
ID: toolCallID,
|
||||
Type: "function",
|
||||
Function: dto.FunctionResponse{
|
||||
Name: tc.Function.Name,
|
||||
|
||||
@@ -21,12 +21,14 @@ func TestOllamaChatHandlerNonStreamToolCalls(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
raw string
|
||||
name string
|
||||
raw string
|
||||
wantID string
|
||||
}{
|
||||
{
|
||||
name: "compact json per-line parse path",
|
||||
raw: `{"model":"llama3.1","created_at":"2026-05-27T12:00:00Z","message":{"role":"assistant","content":"","tool_calls":[{"function":{"name":"get_weather","arguments":{"city":"Paris","days":0}}}]},"done":true,"done_reason":"stop","prompt_eval_count":5,"eval_count":7}`,
|
||||
name: "compact json per-line parse path",
|
||||
raw: `{"model":"llama3.1","created_at":"2026-05-27T12:00:00Z","message":{"role":"assistant","content":"","tool_calls":[{"id":"call_upstream","function":{"name":"get_weather","arguments":{"city":"Paris","days":0}}}]},"done":true,"done_reason":"stop","prompt_eval_count":5,"eval_count":7}`,
|
||||
wantID: "call_upstream",
|
||||
},
|
||||
{
|
||||
name: "pretty json fallback parse path",
|
||||
@@ -53,6 +55,7 @@ func TestOllamaChatHandlerNonStreamToolCalls(t *testing.T) {
|
||||
"prompt_eval_count": 5,
|
||||
"eval_count": 7
|
||||
}`,
|
||||
wantID: "call_0",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,7 +85,7 @@ func TestOllamaChatHandlerNonStreamToolCalls(t *testing.T) {
|
||||
var toolCalls []dto.ToolCallResponse
|
||||
require.NoError(t, common.Unmarshal(out.Choices[0].Message.ToolCalls, &toolCalls))
|
||||
require.Len(t, toolCalls, 1)
|
||||
assert.NotEmpty(t, toolCalls[0].ID)
|
||||
assert.Equal(t, tt.wantID, toolCalls[0].ID)
|
||||
assert.Equal(t, "function", toolCalls[0].Type)
|
||||
assert.Equal(t, "get_weather", toolCalls[0].Function.Name)
|
||||
assert.Nil(t, toolCalls[0].Index)
|
||||
|
||||
Reference in New Issue
Block a user