fix(relay):修复chat协议调用claude协议工具调用问题(适配ZCode)
This commit is contained in:
1 parent
62a23325c7
commit
e6add44afb
25 files changed
+78
-103
No files matched your search
@@ -480,10 +480,11 @@ func appendToolCall(toolCalls []dto.ToolCall, idx *int, part dto.GeminiPart) []d
|
||||
return toolCalls
|
||||
}
|
||||
argsJSON, _ := json.Marshal(part.FunctionCall.Arguments)
|
||||
i := *idx
|
||||
tc := dto.ToolCall{
|
||||
ID: fmt.Sprintf("call_%d", *idx),
|
||||
Type: "function",
|
||||
Index: *idx,
|
||||
Index: &i,
|
||||
Function: dto.FunctionCall{
|
||||
Name: part.FunctionCall.FunctionName,
|
||||
Arguments: string(argsJSON),
|
||||
|
||||
@@ -470,8 +470,9 @@ func legacyOllamaStreamToolCalls(toolCalls []OllamaToolCall, nextIndex *int) []d
|
||||
argsBytes = b
|
||||
}
|
||||
}
|
||||
idx := *nextIndex
|
||||
out = append(out, dto.ToolCall{
|
||||
Index: *nextIndex,
|
||||
Index: &idx,
|
||||
ID: id,
|
||||
Type: "function",
|
||||
Function: dto.FunctionCall{
|
||||
|
||||
@@ -1360,7 +1360,7 @@ func r2cSendToolCallChunk(responseID string, createAt int64, model string, callI
|
||||
if nameByID[callID] != "" {
|
||||
name = nameByID[callID]
|
||||
}
|
||||
tool := dto.ToolCall{ID: callID, Type: "function", Index: idx, Function: dto.FunctionCall{Arguments: argsDelta}}
|
||||
tool := dto.ToolCall{ID: callID, Type: "function", Index: &idx, Function: dto.FunctionCall{Arguments: argsDelta}}
|
||||
if name != "" && !nameSent[callID] {
|
||||
tool.Function.Name = name
|
||||
nameSent[callID] = true
|
||||
|
||||
+10
-6
@@ -49,7 +49,7 @@ type ImageConfigDTO struct {
|
||||
|
||||
// Message OpenAI 消息格式
|
||||
type Message struct {
|
||||
Role string `json:"role"`
|
||||
Role string `json:"role,omitempty"` // 请求/非流式恒有值;流式仅首帧带 role,续帧空值不序列化
|
||||
Content any `json:"content"` // string 或 []ContentPart
|
||||
Refusal *string `json:"refusal,omitempty"` // 结构化输出拒绝原因
|
||||
ReasoningContent *string `json:"reasoning_content,omitempty"` // 推理模型(DeepSeek/o1)的思考内容
|
||||
@@ -103,15 +103,19 @@ type FunctionDef struct {
|
||||
|
||||
// ToolCall 工具调用
|
||||
type ToolCall struct {
|
||||
Index int `json:"index,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
// Index 用指针:流式增量中 index 是客户端归并分片的唯一依据,首个调用的 0 也必须
|
||||
// 序列化(int+omitempty 会把 0 吞掉,客户端把续块当成 name 为空的新调用);
|
||||
// 请求侧与非流式响应不含该字段,留 nil 不序列化。
|
||||
Index *int `json:"index,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Function FunctionCall `json:"function"`
|
||||
}
|
||||
|
||||
// FunctionCall 函数调用
|
||||
// FunctionCall 函数调用(流式续块只带 arguments,id/type/name 空值不得序列化为 "";
|
||||
// arguments 恒带键:codex 等严格 serde 客户端要求首块 "arguments":"" 存在)
|
||||
type FunctionCall struct {
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments string `json:"arguments"`
|
||||
}
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ func (c *ClaudeToOpenAIStreamConverter) ConvertStreamResponse(
|
||||
case "tool_use":
|
||||
// tool_use 块开始
|
||||
toolCall := dto.ToolCall{
|
||||
Index: toolCallIdx,
|
||||
Index: intPtr(toolCallIdx),
|
||||
ID: event.ContentBlock.ID,
|
||||
Type: "function",
|
||||
Function: dto.FunctionCall{
|
||||
@@ -188,7 +188,7 @@ func (c *ClaudeToOpenAIStreamConverter) ConvertStreamResponse(
|
||||
if event.Delta.PartialJSON != nil && *event.Delta.PartialJSON != "" {
|
||||
if err := chunkWriter(newChunk(dto.Message{
|
||||
ToolCalls: []dto.ToolCall{{
|
||||
Index: toolCallIdx - 1,
|
||||
Index: intPtr(toolCallIdx - 1),
|
||||
Function: dto.FunctionCall{
|
||||
Arguments: *event.Delta.PartialJSON,
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"expected_response": {
|
||||
"id": "msg_001",
|
||||
"object": "chat.completion",
|
||||
"created": 1785291101,
|
||||
"created": 1789717794,
|
||||
"model": "claude-3-5-sonnet-20241022",
|
||||
"choices": [
|
||||
{
|
||||
|
||||
@@ -212,10 +212,14 @@ func (c *GeminiToOpenAIStreamConverter) ConvertStreamResponse(
|
||||
// 函数调用
|
||||
if part.FunctionCall != nil {
|
||||
argsJSON, _ := json.Marshal(part.FunctionCall.Arguments)
|
||||
// Gemini 每个函数调用整体到达,但 OpenAI 流式契约要求每个
|
||||
// tool_call 增量都带 index(0 也要发),客户端按 index 归并分片
|
||||
idx := toolCallIdx
|
||||
if err := chunkWriter(newChunk(dto.Message{
|
||||
ToolCalls: []dto.ToolCall{{
|
||||
ID: fmt.Sprintf("call_%s_%d", responseID, toolCallIdx),
|
||||
Type: "function",
|
||||
Index: &idx,
|
||||
ID: fmt.Sprintf("call_%s_%d", responseID, toolCallIdx),
|
||||
Type: "function",
|
||||
Function: dto.FunctionCall{
|
||||
Name: part.FunctionCall.FunctionName,
|
||||
Arguments: string(argsJSON),
|
||||
|
||||
-3
@@ -30,7 +30,6 @@
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "",
|
||||
"content": "Hello!"
|
||||
},
|
||||
"finish_reason": null
|
||||
@@ -47,7 +46,6 @@
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "",
|
||||
"content": " How are you?"
|
||||
},
|
||||
"finish_reason": null
|
||||
@@ -64,7 +62,6 @@
|
||||
{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"role": "",
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop"
|
||||
|
||||
@@ -276,6 +276,11 @@ func (c *OpenAIToResponsesStreamConverter) ConvertStreamResponse(
|
||||
// 工具调用
|
||||
for _, tc := range choice.Delta.ToolCalls {
|
||||
callID := tc.ID
|
||||
// index 缺失时按 0 处理(单工具调用流的兼容兜底)
|
||||
tcIdx := 0
|
||||
if tc.Index != nil {
|
||||
tcIdx = *tc.Index
|
||||
}
|
||||
|
||||
// 新 tool call:有 ID 和 name
|
||||
if callID != "" && tc.Function.Name != "" {
|
||||
@@ -284,7 +289,7 @@ func (c *OpenAIToResponsesStreamConverter) ConvertStreamResponse(
|
||||
return err
|
||||
}
|
||||
// 记录 index → callID 映射,用于后续参数 chunk 的查找
|
||||
toolCallIDByIndex[tc.Index] = callID
|
||||
toolCallIDByIndex[tcIdx] = callID
|
||||
|
||||
// 先关闭文本 content part
|
||||
if !sentTextDone {
|
||||
@@ -317,7 +322,7 @@ func (c *OpenAIToResponsesStreamConverter) ConvertStreamResponse(
|
||||
|
||||
// 参数 chunk:ID 可能为空,通过 index 查找对应的 callID
|
||||
if callID == "" {
|
||||
callID = toolCallIDByIndex[tc.Index]
|
||||
callID = toolCallIDByIndex[tcIdx]
|
||||
}
|
||||
if callID == "" {
|
||||
continue
|
||||
|
||||
@@ -306,7 +306,7 @@ func sendToolCallChunk(chunkWriter func(chunk any) error, responseID string, cre
|
||||
if nameByID[callID] != "" {
|
||||
name = nameByID[callID]
|
||||
}
|
||||
tool := dto.ToolCall{ID: callID, Type: "function", Index: idx, Function: dto.FunctionCall{Arguments: argsDelta}}
|
||||
tool := dto.ToolCall{ID: callID, Type: "function", Index: &idx, Function: dto.FunctionCall{Arguments: argsDelta}}
|
||||
if name != "" && !nameSent[callID] {
|
||||
tool.Function.Name = name
|
||||
nameSent[callID] = true
|
||||
|
||||
@@ -120,7 +120,7 @@ func TestResponsesToOpenAIStream_ToolCalls(t *testing.T) {
|
||||
t.Fatalf("tool chunks = %d, want 3", len(toolChunks))
|
||||
}
|
||||
first := toolChunks[0].Choices[0].Delta.ToolCalls[0]
|
||||
if first.ID != "call_1" || first.Type != "function" || first.Function.Name != "get_weather" || first.Index != 0 {
|
||||
if first.ID != "call_1" || first.Type != "function" || first.Function.Name != "get_weather" || first.Index == nil || *first.Index != 0 {
|
||||
t.Errorf("first tool chunk = %+v", first)
|
||||
}
|
||||
// name 只在首个 chunk 下发一次
|
||||
|
||||
@@ -424,7 +424,7 @@ func TestOllamaToOpenAIStreamConverter_ToolCalls(t *testing.T) {
|
||||
t.Fatalf("tool delta = %+v", toolDelta.ToolCalls)
|
||||
}
|
||||
tc := toolDelta.ToolCalls[0]
|
||||
if tc.Index != 0 || tc.ID != "call_1" || tc.Type != "function" {
|
||||
if tc.Index == nil || *tc.Index != 0 || tc.ID != "call_1" || tc.Type != "function" {
|
||||
t.Errorf("tool call = %+v", tc)
|
||||
}
|
||||
if tc.Function.Name != "get_weather" || tc.Function.Arguments != `{"city":"Paris"}` {
|
||||
|
||||
@@ -178,8 +178,9 @@ func ollamaStreamToolCalls(toolCalls []dto.OllamaToolCall, nextIndex *int) []dto
|
||||
argsBytes = b
|
||||
}
|
||||
}
|
||||
idx := *nextIndex
|
||||
out = append(out, dto.ToolCall{
|
||||
Index: *nextIndex,
|
||||
Index: &idx,
|
||||
ID: id,
|
||||
Type: "function",
|
||||
Function: dto.FunctionCall{
|
||||
|
||||
+3
-6
@@ -23,8 +23,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Hello",
|
||||
"role": ""
|
||||
"content": "Hello"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -42,8 +41,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": ", world!",
|
||||
"role": ""
|
||||
"content": ", world!"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -61,8 +59,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -24,8 +24,7 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"reasoning_content": "Let me think...",
|
||||
"role": ""
|
||||
"reasoning_content": "Let me think..."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -43,8 +42,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "The answer is 42.",
|
||||
"role": ""
|
||||
"content": "The answer is 42."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -62,8 +60,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+7
-15
@@ -23,8 +23,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Let me check the weather.",
|
||||
"role": ""
|
||||
"content": "Let me check the weather."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -43,7 +42,6 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
@@ -51,6 +49,7 @@
|
||||
"name": "get_weather"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"index": 0,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -72,15 +71,12 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{\"city\":",
|
||||
"name": ""
|
||||
"arguments": "{\"city\":"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"type": ""
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -101,15 +97,12 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "\"Shanghai\"}",
|
||||
"name": ""
|
||||
"arguments": "\"Shanghai\"}"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"type": ""
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -129,8 +122,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -23,8 +23,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Hello",
|
||||
"role": ""
|
||||
"content": "Hello"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -42,8 +41,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": ", world!",
|
||||
"role": ""
|
||||
"content": ", world!"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -61,8 +59,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -24,8 +24,7 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"reasoning_content": "Let me think...",
|
||||
"role": ""
|
||||
"reasoning_content": "Let me think..."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -43,8 +42,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "The answer is 42.",
|
||||
"role": ""
|
||||
"content": "The answer is 42."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -62,8 +60,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+3
-5
@@ -23,8 +23,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Let me check the weather.",
|
||||
"role": ""
|
||||
"content": "Let me check the weather."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -43,7 +42,6 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
@@ -51,6 +49,7 @@
|
||||
"name": "get_weather"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"index": 0,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -71,8 +70,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -23,8 +23,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Hello",
|
||||
"role": ""
|
||||
"content": "Hello"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -42,8 +41,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": ", world!",
|
||||
"role": ""
|
||||
"content": ", world!"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -61,8 +59,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -24,8 +24,7 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"reasoning_content": "Let me think...",
|
||||
"role": ""
|
||||
"reasoning_content": "Let me think..."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -43,8 +42,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "The answer is 42.",
|
||||
"role": ""
|
||||
"content": "The answer is 42."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -62,8 +60,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+6
-9
@@ -5,7 +5,6 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
@@ -13,6 +12,7 @@
|
||||
"name": "get_weather"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"index": 0,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -34,14 +34,13 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "{\"city\":",
|
||||
"name": ""
|
||||
"arguments": "{\"city\":"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"index": 0,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -63,14 +62,13 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"arguments": "\"Shanghai\"}",
|
||||
"name": ""
|
||||
"arguments": "\"Shanghai\"}"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"index": 0,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -110,8 +108,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -23,8 +23,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "Hello",
|
||||
"role": ""
|
||||
"content": "Hello"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -42,8 +41,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": ", world!",
|
||||
"role": ""
|
||||
"content": ", world!"
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -61,8 +59,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+3
-6
@@ -24,8 +24,7 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": "",
|
||||
"reasoning_content": "Let me think...",
|
||||
"role": ""
|
||||
"reasoning_content": "Let me think..."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -43,8 +42,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": "The answer is 42.",
|
||||
"role": ""
|
||||
"content": "The answer is 42."
|
||||
},
|
||||
"finish_reason": null,
|
||||
"index": 0
|
||||
@@ -62,8 +60,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
"index": 0
|
||||
|
||||
+2
-3
@@ -24,7 +24,6 @@
|
||||
{
|
||||
"delta": {
|
||||
"content": "",
|
||||
"role": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
@@ -32,6 +31,7 @@
|
||||
"name": "get_weather"
|
||||
},
|
||||
"id": "\u003cscrubbed\u003e",
|
||||
"index": 0,
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
@@ -52,8 +52,7 @@
|
||||
"choices": [
|
||||
{
|
||||
"delta": {
|
||||
"content": null,
|
||||
"role": ""
|
||||
"content": null
|
||||
},
|
||||
"finish_reason": "tool_calls",
|
||||
"index": 0
|
||||
|
||||
Reference in new issue
Block a user