Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
8ad159a3
authored
Aug 10, 2026
by
Seefs
Committed by
GitHub
Aug 10, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(ollama): preserve reasoning and tool-call context (#6605)
parent
85feb7a3
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
108 additions
and
46 deletions
+108
-46
relay/channel/ollama/dto.go
+4
-2
relay/channel/ollama/relay-ollama.go
+94
-41
relay/channel/ollama/stream.go
+5
-1
relay/channel/ollama/stream_test.go
+5
-2
No files found.
relay/channel/ollama/dto.go
View file @
8ad159a3
...
...
@@ -10,6 +10,7 @@ type OllamaChatMessage struct {
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"`
}
...
...
@@ -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"`
...
...
relay/channel/ollama/relay-ollama.go
View file @
8ad159a3
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
,
}
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
}
Think
:
think
,
}
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
=
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
.
Tools
=
tools
})
}
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
!=
""
{
_
=
js
on
.
Unmarshal
([]
byte
(
tc
.
Function
.
Arguments
),
&
args
)
_
=
comm
on
.
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
:=
js
on
.
Unmarshal
(
body
,
&
versionResp
);
err
!=
nil
{
if
err
:=
comm
on
.
Unmarshal
(
body
,
&
versionResp
);
err
!=
nil
{
return
""
,
fmt
.
Errorf
(
"解析响应失败: %v"
,
err
)
}
...
...
relay/channel/ollama/stream.go
View file @
8ad159a3
...
...
@@ -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
,
...
...
relay/channel/ollama/stream_test.go
View file @
8ad159a3
...
...
@@ -23,10 +23,12 @@ func TestOllamaChatHandlerNonStreamToolCalls(t *testing.T) {
tests
:=
[]
struct
{
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}`
,
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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment