mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix(apicompat): 回程还原 namespace 子工具调用,修复 Codex MCP 工具 unsupported call
Codex 0.14x 将 MCP 工具声明为 namespace 工具,chat 桥去程摊平为 "<namespace>__<name>" function 工具后,回程仅原样回传平铺名的 function_call 项;codex 按 namespace+name 路由查不到该名字,所有 MCP 工具调用被判为 unsupported call。 - NamespaceToolNames 构建摊平名 →(namespace, 子工具名)反查表 (摊平名超长带截断哈希,无法按字符串切分还原) - 非流式/流式回程命中映射时还原为裸子工具名 + namespace 字段, ResponsesOutput 新增 Namespace 字段并同步 wire 层白名单 - 回退桥入口将映射与 CustomTools/ToolSearchDeclared 一并穿入 已在测试机经 codex exec + MCP server 端到端验证。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NWQyEgFKKbdve67G6qCoAU
This commit is contained in:
@@ -64,6 +64,42 @@ func CustomToolNames(tools []ResponsesTool) map[string]bool {
|
||||
return out
|
||||
}
|
||||
|
||||
// NamespacedToolName 记录 namespace 子工具的原始归属(命名空间 + 裸子工具名)。
|
||||
type NamespacedToolName struct {
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
// NamespaceToolNames 收集 Responses 请求中 namespace 子工具的摊平名 →(namespace,
|
||||
// 子工具名)映射。chat 桥回程时需据此把模型对摊平工具的调用还原为带 namespace 字段
|
||||
// 的 function_call 项:codex 按 namespace+name 路由,平铺名会被判为 unsupported
|
||||
// call;摊平名超长时带截断哈希(见 flattenNamespaceToolName),无法按字符串切分还原。
|
||||
func NamespaceToolNames(tools []ResponsesTool) map[string]NamespacedToolName {
|
||||
var out map[string]NamespacedToolName
|
||||
for _, tool := range tools {
|
||||
if tool.Type != "namespace" || tool.Name == "" {
|
||||
continue
|
||||
}
|
||||
children := tool.Tools
|
||||
if len(children) == 0 {
|
||||
children = tool.Children
|
||||
}
|
||||
for _, child := range children {
|
||||
if child.Type != "function" || child.Name == "" {
|
||||
continue
|
||||
}
|
||||
if out == nil {
|
||||
out = make(map[string]NamespacedToolName)
|
||||
}
|
||||
out[flattenNamespaceToolName(tool.Name, child.Name)] = NamespacedToolName{
|
||||
Namespace: tool.Name,
|
||||
Name: child.Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// HasToolSearchTool 判断 Responses 请求是否声明了 tool_search 服务端工具。chat 桥
|
||||
// 回程时需据此把模型对代理工具的调用还原为 tool_search_call 项:codex 只在该项类型
|
||||
// 且 execution=client 时执行 tool search,同名 function_call 会因 payload 不匹配
|
||||
@@ -679,8 +715,9 @@ func extractCustomToolCallInput(arguments string) string {
|
||||
// response into a Responses API response. customTools 是客户端请求中 custom 工具
|
||||
// 的名字集合(见 CustomToolNames),命中的调用会还原为 custom_tool_call 项;
|
||||
// toolSearch 表示客户端声明了 tool_search 工具(见 HasToolSearchTool),代理工具
|
||||
// 的调用会还原为 tool_search_call 项。
|
||||
func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools map[string]bool, toolSearch bool) *ResponsesResponse {
|
||||
// 的调用会还原为 tool_search_call 项;namespaceTools 是 namespace 子工具的摊平名
|
||||
// 映射(见 NamespaceToolNames),命中的调用还原为带 namespace 字段的 function_call 项。
|
||||
func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model string, customTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) *ResponsesResponse {
|
||||
id := ""
|
||||
if resp != nil {
|
||||
id = resp.ID
|
||||
@@ -705,7 +742,7 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str
|
||||
|
||||
if len(resp.Choices) > 0 {
|
||||
choice := resp.Choices[0]
|
||||
out.Output = chatMessageToResponsesOutput(choice.Message, customTools, toolSearch)
|
||||
out.Output = chatMessageToResponsesOutput(choice.Message, customTools, toolSearch, namespaceTools)
|
||||
if choice.FinishReason == "length" {
|
||||
out.Status = "incomplete"
|
||||
out.IncompleteDetails = &ResponsesIncompleteDetails{Reason: "max_output_tokens"}
|
||||
@@ -720,7 +757,7 @@ func ChatCompletionsResponseToResponses(resp *ChatCompletionsResponse, model str
|
||||
return out
|
||||
}
|
||||
|
||||
func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bool, toolSearch bool) []ResponsesOutput {
|
||||
func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bool, toolSearch bool, namespaceTools map[string]NamespacedToolName) []ResponsesOutput {
|
||||
var outputs []ResponsesOutput
|
||||
if message.ReasoningContent != "" {
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
@@ -776,6 +813,18 @@ func chatMessageToResponsesOutput(message ChatMessage, customTools map[string]bo
|
||||
})
|
||||
continue
|
||||
}
|
||||
if ns, ok := namespaceTools[toolCall.Function.Name]; ok {
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
Type: "function_call",
|
||||
ID: generateItemID(),
|
||||
CallID: toolCall.ID,
|
||||
Name: ns.Name,
|
||||
Namespace: ns.Namespace,
|
||||
Arguments: arguments,
|
||||
Status: "completed",
|
||||
})
|
||||
continue
|
||||
}
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
Type: "function_call",
|
||||
ID: generateItemID(),
|
||||
@@ -905,6 +954,11 @@ type ChatCompletionsToResponsesStreamState struct {
|
||||
// 该项类型(且 execution=client)执行 tool search。
|
||||
ToolSearchDeclared bool
|
||||
|
||||
// NamespaceTools 是 namespace 子工具的摊平名 → 原始归属映射(见
|
||||
// NamespaceToolNames)。命中的调用还原为带 namespace 字段的 function_call 项,
|
||||
// codex 按 namespace+name 路由。
|
||||
NamespaceTools map[string]NamespacedToolName
|
||||
|
||||
// toolIsCustom 记录每个工具调用宣告时的类型判定,保证 added/done 事件的
|
||||
// 项类型一致。
|
||||
toolIsCustom map[int]bool
|
||||
@@ -912,6 +966,9 @@ type ChatCompletionsToResponsesStreamState struct {
|
||||
// toolIsToolSearch 记录工具调用是否判定为 tool_search 代理调用。
|
||||
toolIsToolSearch map[int]bool
|
||||
|
||||
// toolNamespace 记录工具调用宣告时命中的 namespace 归属(见 NamespaceTools)。
|
||||
toolNamespace map[int]NamespacedToolName
|
||||
|
||||
// toolAnnounced 记录 output_item.added 是否已发出。存在 custom 工具且名字
|
||||
// 尚未到达时延迟宣告,待名字可判定类型后再补发(见 announceChatToolItem)。
|
||||
toolAnnounced map[int]bool
|
||||
@@ -931,6 +988,7 @@ func NewChatCompletionsToResponsesStreamState(model string) *ChatCompletionsToRe
|
||||
ToolOutputIndex: make(map[int]int),
|
||||
toolIsCustom: make(map[int]bool),
|
||||
toolIsToolSearch: make(map[int]bool),
|
||||
toolNamespace: make(map[int]NamespacedToolName),
|
||||
toolAnnounced: make(map[int]bool),
|
||||
}
|
||||
}
|
||||
@@ -1263,7 +1321,7 @@ func announceChatToolItem(
|
||||
if state.toolAnnounced[idx] {
|
||||
return nil
|
||||
}
|
||||
if !force && stored.Function.Name == "" && (len(state.CustomTools) > 0 || state.ToolSearchDeclared) {
|
||||
if !force && stored.Function.Name == "" && (len(state.CustomTools) > 0 || state.ToolSearchDeclared || len(state.NamespaceTools) > 0) {
|
||||
return nil
|
||||
}
|
||||
state.toolAnnounced[idx] = true
|
||||
@@ -1278,14 +1336,22 @@ func announceChatToolItem(
|
||||
if isToolSearch {
|
||||
itemType = "tool_search_call"
|
||||
}
|
||||
// namespace 子工具的调用仍按 function_call 生命周期下发,但 added/done 项要
|
||||
// 还原为裸子工具名 + namespace 字段(codex 按 namespace+name 路由)。
|
||||
itemName, itemNamespace := stored.Function.Name, ""
|
||||
if ns, ok := state.NamespaceTools[stored.Function.Name]; ok && !isCustom && !isToolSearch {
|
||||
state.toolNamespace[idx] = ns
|
||||
itemName, itemNamespace = ns.Name, ns.Namespace
|
||||
}
|
||||
events := []ResponsesStreamEvent{chatToResponsesEvent(state, "response.output_item.added", &ResponsesStreamEvent{
|
||||
OutputIndex: state.ToolOutputIndex[idx],
|
||||
Item: &ResponsesOutput{
|
||||
Type: itemType,
|
||||
ID: state.ToolItemIDs[idx],
|
||||
CallID: stored.ID,
|
||||
Name: stored.Function.Name,
|
||||
Status: "in_progress",
|
||||
Type: itemType,
|
||||
ID: state.ToolItemIDs[idx],
|
||||
CallID: stored.ID,
|
||||
Name: itemName,
|
||||
Namespace: itemNamespace,
|
||||
Status: "in_progress",
|
||||
},
|
||||
})}
|
||||
// 迟到宣告时补发已累积的参数增量(custom/tool_search 的输入收尾统一下发,不补发)。
|
||||
@@ -1374,12 +1440,17 @@ func closeChatToolItems(state *ChatCompletionsToResponsesStreamState) []Response
|
||||
}))
|
||||
continue
|
||||
}
|
||||
// namespace 子工具调用在宣告时已记录归属,收尾项同样带还原名与 namespace。
|
||||
name, namespace := toolCall.Function.Name, ""
|
||||
if ns, ok := state.toolNamespace[i]; ok {
|
||||
name, namespace = ns.Name, ns.Namespace
|
||||
}
|
||||
events = append(events,
|
||||
chatToResponsesEvent(state, "response.function_call_arguments.done", &ResponsesStreamEvent{
|
||||
OutputIndex: outputIndex,
|
||||
ItemID: itemID,
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: name,
|
||||
Arguments: arguments,
|
||||
}),
|
||||
chatToResponsesEvent(state, "response.output_item.done", &ResponsesStreamEvent{
|
||||
@@ -1388,7 +1459,8 @@ func closeChatToolItems(state *ChatCompletionsToResponsesStreamState) []Response
|
||||
Type: "function_call",
|
||||
ID: itemID,
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Arguments: arguments,
|
||||
Status: "completed",
|
||||
},
|
||||
@@ -1452,11 +1524,16 @@ func (state *ChatCompletionsToResponsesStreamState) chatOutput() []ResponsesOutp
|
||||
})
|
||||
continue
|
||||
}
|
||||
name, namespace := toolCall.Function.Name, ""
|
||||
if ns, ok := state.toolNamespace[i]; ok {
|
||||
name, namespace = ns.Name, ns.Namespace
|
||||
}
|
||||
outputs = append(outputs, ResponsesOutput{
|
||||
Type: "function_call",
|
||||
ID: generateItemID(),
|
||||
CallID: toolCall.ID,
|
||||
Name: toolCall.Function.Name,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Arguments: arguments,
|
||||
Status: "completed",
|
||||
})
|
||||
|
||||
+187
-3
@@ -103,7 +103,7 @@ func TestChatCompletionsResponseToResponses_CustomToolCallOutputItem(t *testing.
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", map[string]bool{"exec": true}, false)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", map[string]bool{"exec": true}, false, nil)
|
||||
require.Len(t, out.Output, 2)
|
||||
|
||||
assert.Equal(t, "custom_tool_call", out.Output[0].Type)
|
||||
@@ -226,7 +226,7 @@ func TestChatCompletionsResponseToResponses_ToolSearchCallOutputItem(t *testing.
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, true)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, true, nil)
|
||||
require.Len(t, out.Output, 1)
|
||||
|
||||
item := out.Output[0]
|
||||
@@ -258,7 +258,7 @@ func TestChatCompletionsResponseToResponses_ToolSearchNotDeclaredKeepsFunctionCa
|
||||
}
|
||||
|
||||
// 客户端未声明 type=tool_search 时,同名普通 function 工具不受影响。
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, false)
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, false, nil)
|
||||
require.Len(t, out.Output, 1)
|
||||
assert.Equal(t, "function_call", out.Output[0].Type)
|
||||
}
|
||||
@@ -506,6 +506,190 @@ func TestResponsesEventToSSE_CustomToolCallItemCarriesAllFields(t *testing.T) {
|
||||
assert.Contains(t, sse, `"type":"custom_tool_call"`)
|
||||
}
|
||||
|
||||
func TestNamespaceToolNames_MapsFlattenedNames(t *testing.T) {
|
||||
tools := []ResponsesTool{
|
||||
{Type: "namespace", Name: "gmail", Tools: []ResponsesTool{
|
||||
{Type: "function", Name: "send"},
|
||||
{Type: "custom", Name: "skip_me"},
|
||||
}},
|
||||
{Type: "namespace", Name: "crm", Children: []ResponsesTool{
|
||||
{Type: "function", Name: "query"},
|
||||
}},
|
||||
{Type: "function", Name: "wait"},
|
||||
}
|
||||
|
||||
m := NamespaceToolNames(tools)
|
||||
require.Len(t, m, 2)
|
||||
assert.Equal(t, NamespacedToolName{Namespace: "gmail", Name: "send"}, m["gmail__send"])
|
||||
assert.Equal(t, NamespacedToolName{Namespace: "crm", Name: "query"}, m["crm__query"])
|
||||
|
||||
// 摊平名超长时截断加哈希,无法按字符串切分还原,必须经映射反查。
|
||||
longNS := "very_long_namespace_prefix_for_testing_purposes"
|
||||
longChild := "and_a_rather_long_tool_name_too"
|
||||
m2 := NamespaceToolNames([]ResponsesTool{{
|
||||
Type: "namespace", Name: longNS,
|
||||
Tools: []ResponsesTool{{Type: "function", Name: longChild}},
|
||||
}})
|
||||
assert.Equal(t, NamespacedToolName{Namespace: longNS, Name: longChild},
|
||||
m2[flattenNamespaceToolName(longNS, longChild)])
|
||||
|
||||
assert.Nil(t, NamespaceToolNames(nil))
|
||||
}
|
||||
|
||||
// codex 按 namespace+name 路由 namespace 子工具的调用:回程必须把摊平名还原为
|
||||
// 裸子工具名并带独立 namespace 字段,平铺名的 function_call 会被 codex 判为
|
||||
// unsupported call 拒绝执行。
|
||||
func TestChatCompletionsResponseToResponses_NamespacedToolCallRestored(t *testing.T) {
|
||||
resp := &ChatCompletionsResponse{
|
||||
ID: "cc-1",
|
||||
Choices: []ChatChoice{{
|
||||
Message: ChatMessage{
|
||||
Role: "assistant",
|
||||
ToolCalls: []ChatToolCall{
|
||||
{ID: "call_n", Function: ChatFunctionCall{Name: "mcp__svc__echo", Arguments: `{"text":"hi"}`}},
|
||||
{ID: "call_9", Function: ChatFunctionCall{Name: "wait", Arguments: `{"cell_id": 3}`}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
nsTools := map[string]NamespacedToolName{
|
||||
"mcp__svc__echo": {Namespace: "mcp__svc", Name: "echo"},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "glm-5.2", nil, false, nsTools)
|
||||
require.Len(t, out.Output, 2)
|
||||
|
||||
item := out.Output[0]
|
||||
assert.Equal(t, "function_call", item.Type)
|
||||
assert.Equal(t, "echo", item.Name)
|
||||
assert.Equal(t, "mcp__svc", item.Namespace)
|
||||
assert.Equal(t, "call_n", item.CallID)
|
||||
assert.Equal(t, `{"text":"hi"}`, item.Arguments)
|
||||
|
||||
// 非流式响应体走 ResponsesOutput.MarshalJSON,namespace 必须落到线上 JSON。
|
||||
b, err := json.Marshal(item)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(b), `"namespace":"mcp__svc"`)
|
||||
assert.Contains(t, string(b), `"name":"echo"`)
|
||||
|
||||
// 未命中映射的普通 function 调用不受影响,且不携带 namespace 字段。
|
||||
assert.Equal(t, "wait", out.Output[1].Name)
|
||||
assert.Empty(t, out.Output[1].Namespace)
|
||||
b2, err := json.Marshal(out.Output[1])
|
||||
require.NoError(t, err)
|
||||
assert.NotContains(t, string(b2), `"namespace"`)
|
||||
}
|
||||
|
||||
func TestChatCompletionsChunkToResponsesEvents_NamespacedToolCallStream(t *testing.T) {
|
||||
state := NewChatCompletionsToResponsesStreamState("glm-5.2")
|
||||
state.NamespaceTools = map[string]NamespacedToolName{
|
||||
"mcp__svc__echo": {Namespace: "mcp__svc", Name: "echo"},
|
||||
}
|
||||
|
||||
idx := 0
|
||||
chunk := &ChatCompletionsChunk{
|
||||
ID: "cc-1",
|
||||
Choices: []ChatChunkChoice{{
|
||||
Delta: ChatDelta{
|
||||
ToolCalls: []ChatToolCall{{
|
||||
Index: &idx,
|
||||
ID: "call_n",
|
||||
Function: ChatFunctionCall{Name: "mcp__svc__echo", Arguments: `{"text":"hi"}`},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
events := ChatCompletionsChunkToResponsesEvents(chunk, state)
|
||||
events = append(events, FinalizeChatCompletionsResponsesStream(state)...)
|
||||
|
||||
var added, itemDone *ResponsesStreamEvent
|
||||
for i := range events {
|
||||
evt := &events[i]
|
||||
switch evt.Type {
|
||||
case "response.output_item.added":
|
||||
if evt.Item != nil && evt.Item.Type != "message" && evt.Item.Type != "reasoning" {
|
||||
added = evt
|
||||
}
|
||||
case "response.output_item.done":
|
||||
if evt.Item != nil && evt.Item.Type == "function_call" {
|
||||
itemDone = evt
|
||||
}
|
||||
case "response.custom_tool_call_input.delta", "response.custom_tool_call_input.done":
|
||||
t.Fatalf("namespace 子工具调用不应产出 custom 事件: %s", evt.Type)
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, added, "缺少 namespace 调用的 output_item.added")
|
||||
assert.Equal(t, "function_call", added.Item.Type)
|
||||
assert.Equal(t, "echo", added.Item.Name)
|
||||
assert.Equal(t, "mcp__svc", added.Item.Namespace)
|
||||
|
||||
require.NotNil(t, itemDone, "缺少 namespace 调用的 output_item.done")
|
||||
assert.Equal(t, "call_n", itemDone.Item.CallID)
|
||||
assert.Equal(t, "echo", itemDone.Item.Name)
|
||||
assert.Equal(t, "mcp__svc", itemDone.Item.Namespace)
|
||||
assert.Equal(t, `{"text":"hi"}`, itemDone.Item.Arguments)
|
||||
|
||||
// SSE 线上形态经 responsesItemWire 白名单重组,必须单独断言 namespace 落线。
|
||||
sse, err := ResponsesEventToSSE(*itemDone)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, sse, `"namespace":"mcp__svc"`)
|
||||
assert.Contains(t, sse, `"name":"echo"`)
|
||||
assert.Contains(t, sse, `"call_id":"call_n"`)
|
||||
|
||||
// response.completed 的 output 数组同样携带还原后的 namespace 调用项。
|
||||
final := events[len(events)-1]
|
||||
require.Equal(t, "response.completed", final.Type)
|
||||
require.NotNil(t, final.Response)
|
||||
found := false
|
||||
for _, item := range final.Response.Output {
|
||||
if item.Type == "function_call" {
|
||||
found = true
|
||||
assert.Equal(t, "echo", item.Name)
|
||||
assert.Equal(t, "mcp__svc", item.Namespace)
|
||||
}
|
||||
}
|
||||
assert.True(t, found, "response.completed 缺少还原后的 namespace 调用项")
|
||||
}
|
||||
|
||||
func TestChatCompletionsChunkToResponsesEvents_NamespacedToolNameArrivesLate(t *testing.T) {
|
||||
state := NewChatCompletionsToResponsesStreamState("glm-5.2")
|
||||
state.NamespaceTools = map[string]NamespacedToolName{
|
||||
"mcp__svc__echo": {Namespace: "mcp__svc", Name: "echo"},
|
||||
}
|
||||
|
||||
idx := 0
|
||||
chunk1 := &ChatCompletionsChunk{Choices: []ChatChunkChoice{{Delta: ChatDelta{
|
||||
ToolCalls: []ChatToolCall{{Index: &idx, ID: "call_n", Function: ChatFunctionCall{Arguments: `{"te`}}},
|
||||
}}}}
|
||||
chunk2 := &ChatCompletionsChunk{Choices: []ChatChunkChoice{{Delta: ChatDelta{
|
||||
ToolCalls: []ChatToolCall{{Index: &idx, Function: ChatFunctionCall{Name: "mcp__svc__echo", Arguments: `xt":"hi"}`}}},
|
||||
}}}}
|
||||
|
||||
var events []ResponsesStreamEvent
|
||||
events = append(events, ChatCompletionsChunkToResponsesEvents(chunk1, state)...)
|
||||
events = append(events, ChatCompletionsChunkToResponsesEvents(chunk2, state)...)
|
||||
events = append(events, FinalizeChatCompletionsResponsesStream(state)...)
|
||||
|
||||
addedCount := 0
|
||||
deltas := ""
|
||||
for _, evt := range events {
|
||||
switch evt.Type {
|
||||
case "response.output_item.added":
|
||||
if evt.Item != nil && evt.Item.Type != "reasoning" && evt.Item.Type != "message" {
|
||||
addedCount++
|
||||
assert.Equal(t, "echo", evt.Item.Name, "迟到的名字命中 namespace 映射时按还原名宣告")
|
||||
assert.Equal(t, "mcp__svc", evt.Item.Namespace)
|
||||
}
|
||||
case "response.function_call_arguments.delta":
|
||||
deltas += evt.Delta
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, addedCount, "工具调用只宣告一次")
|
||||
assert.Equal(t, `{"text":"hi"}`, deltas, "宣告前累积的参数需在宣告时补发")
|
||||
}
|
||||
|
||||
func TestChatCompletionsChunkToResponsesEvents_FunctionToolStreamUnaffected(t *testing.T) {
|
||||
state := NewChatCompletionsToResponsesStreamState("glm-5.2")
|
||||
state.CustomTools = map[string]bool{"exec": true}
|
||||
|
||||
@@ -418,7 +418,7 @@ func TestChatCompletionsResponseToResponses_DeepSeekReasoningOnlyFallsBackToMess
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false)
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false, nil)
|
||||
|
||||
require.Len(t, out.Output, 2)
|
||||
require.Equal(t, "reasoning", out.Output[0].Type)
|
||||
@@ -452,7 +452,7 @@ func TestChatCompletionsResponseToResponses_DeepSeekReasoningToolCallDoesNotFall
|
||||
}},
|
||||
}
|
||||
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false)
|
||||
out := ChatCompletionsResponseToResponses(resp, "deepseek-reasoner", nil, false, nil)
|
||||
|
||||
require.Len(t, out.Output, 2)
|
||||
require.Equal(t, "reasoning", out.Output[0].Type)
|
||||
|
||||
@@ -184,6 +184,11 @@ func responsesItemWire(item *ResponsesOutput) map[string]any {
|
||||
m["call_id"] = item.CallID
|
||||
m["name"] = item.Name
|
||||
m["arguments"] = item.Arguments
|
||||
// namespace 子工具的还原调用:codex 按 namespace+name 路由,缺少该字段
|
||||
// 会被判为 unsupported call。
|
||||
if item.Namespace != "" {
|
||||
m["namespace"] = item.Namespace
|
||||
}
|
||||
case "custom_tool_call":
|
||||
// custom/freeform 工具调用(如 codex 的 exec):input 为自由文本。缺少
|
||||
// call_id/name 时 codex 无法路由该调用(表现为 unsupported call)。
|
||||
|
||||
@@ -321,6 +321,8 @@ type ResponsesOutput struct {
|
||||
CallID string `json:"call_id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments string `json:"arguments,omitempty"`
|
||||
// 来源为 namespace 子工具时的归属命名空间(codex 按 namespace+name 路由该调用)。
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
// type=custom_tool_call(custom/freeform 工具,input 为自由文本)
|
||||
Input string `json:"input,omitempty"`
|
||||
|
||||
@@ -140,7 +140,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsAnthropic(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responsesResp := apicompat.ChatCompletionsResponseToResponses(ccResp, originalModel, nil, false)
|
||||
responsesResp := apicompat.ChatCompletionsResponseToResponses(ccResp, originalModel, nil, false, nil)
|
||||
|
||||
anthropicResp := apicompat.ResponsesToAnthropic(responsesResp, originalModel)
|
||||
|
||||
|
||||
@@ -42,9 +42,11 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions(
|
||||
serviceTier := extractOpenAIServiceTierFromBody(body)
|
||||
// custom 工具(如 codex 的 exec)降级为 function 工具转发,回程需按名字还原为
|
||||
// custom_tool_call 项,先记下名字集合;tool_search 工具同理,回程还原为
|
||||
// tool_search_call 项。
|
||||
// tool_search_call 项;namespace 子工具(如 MCP 工具)摊平转发,回程按映射还原
|
||||
// 为带 namespace 字段的 function_call 项。
|
||||
customTools := apicompat.CustomToolNames(responsesReq.Tools)
|
||||
toolSearch := apicompat.HasToolSearchTool(responsesReq.Tools)
|
||||
namespaceTools := apicompat.NamespaceToolNames(responsesReq.Tools)
|
||||
|
||||
chatReq, err := apicompat.ResponsesToChatCompletionsRequest(&responsesReq)
|
||||
if err != nil {
|
||||
@@ -105,9 +107,9 @@ func (s *OpenAIGatewayService) forwardResponsesViaRawChatCompletions(
|
||||
}
|
||||
|
||||
if clientStream {
|
||||
return s.streamChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
return s.streamChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, namespaceTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
}
|
||||
return s.bufferChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
return s.bufferChatCompletionsAsResponses(c, resp, originalModel, customTools, toolSearch, namespaceTools, billingModel, upstreamModel, reasoningEffort, serviceTier, startTime)
|
||||
}
|
||||
|
||||
func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses(
|
||||
@@ -116,6 +118,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses(
|
||||
originalModel string,
|
||||
customTools map[string]bool,
|
||||
toolSearch bool,
|
||||
namespaceTools map[string]apicompat.NamespacedToolName,
|
||||
billingModel string,
|
||||
upstreamModel string,
|
||||
reasoningEffort *string,
|
||||
@@ -127,7 +130,7 @@ func (s *OpenAIGatewayService) bufferChatCompletionsAsResponses(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
responsesResp := apicompat.ChatCompletionsResponseToResponses(ccResp, originalModel, customTools, toolSearch)
|
||||
responsesResp := apicompat.ChatCompletionsResponseToResponses(ccResp, originalModel, customTools, toolSearch, namespaceTools)
|
||||
|
||||
if s.responseHeaderFilter != nil {
|
||||
responseheaders.WriteFilteredHeaders(c.Writer.Header(), resp.Header, s.responseHeaderFilter)
|
||||
@@ -153,6 +156,7 @@ func (s *OpenAIGatewayService) streamChatCompletionsAsResponses(
|
||||
originalModel string,
|
||||
customTools map[string]bool,
|
||||
toolSearch bool,
|
||||
namespaceTools map[string]apicompat.NamespacedToolName,
|
||||
billingModel string,
|
||||
upstreamModel string,
|
||||
reasoningEffort *string,
|
||||
@@ -165,6 +169,7 @@ func (s *OpenAIGatewayService) streamChatCompletionsAsResponses(
|
||||
state := apicompat.NewChatCompletionsToResponsesStreamState(originalModel)
|
||||
state.CustomTools = customTools
|
||||
state.ToolSearchDeclared = toolSearch
|
||||
state.NamespaceTools = namespaceTools
|
||||
clientDisconnected := false
|
||||
|
||||
writeEvents := func(events []apicompat.ResponsesStreamEvent) {
|
||||
|
||||
Reference in New Issue
Block a user