Merge pull request #1910 from slovx2/fix/codex-tool-call-ids

fix(openai): 修复 Codex 工具调用 call_id 处理
This commit is contained in:
Wesley Liddick
2026-04-24 19:56:03 +08:00
committed by GitHub
4 changed files with 94 additions and 7 deletions

View File

@@ -658,12 +658,14 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
} }
} }
if !isCodexToolCallItemType(typ) {
ensureCopy()
delete(newItem, "call_id")
}
if !preserveReferences { if !preserveReferences {
ensureCopy() ensureCopy()
delete(newItem, "id") delete(newItem, "id")
if !isCodexToolCallItemType(typ) {
delete(newItem, "call_id")
}
} }
filtered = append(filtered, newItem) filtered = append(filtered, newItem)
@@ -672,10 +674,20 @@ func filterCodexInput(input []any, preserveReferences bool) []any {
} }
func isCodexToolCallItemType(typ string) bool { func isCodexToolCallItemType(typ string) bool {
if typ == "" { switch typ {
case "function_call",
"tool_call",
"local_shell_call",
"tool_search_call",
"custom_tool_call",
"function_call_output",
"mcp_tool_call_output",
"custom_tool_call_output",
"tool_search_output":
return true
default:
return false return false
} }
return strings.HasSuffix(typ, "_call") || strings.HasSuffix(typ, "_call_output")
} }
func normalizeCodexTools(reqBody map[string]any) bool { func normalizeCodexTools(reqBody map[string]any) bool {

View File

@@ -92,6 +92,78 @@ func TestApplyCodexOAuthTransform_ToolContinuationNormalizesToolReferenceIDsOnly
require.Equal(t, "fc1", second["call_id"]) require.Equal(t, "fc1", second["call_id"])
} }
func TestApplyCodexOAuthTransform_ToolSearchOutputPreservesCallID(t *testing.T) {
reqBody := map[string]any{
"model": "gpt-5.2",
"input": []any{
map[string]any{"type": "tool_search_output", "call_id": "call_1", "output": "ok"},
},
}
applyCodexOAuthTransform(reqBody, false, false)
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 1)
first, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "tool_search_output", first["type"])
require.Equal(t, "fc1", first["call_id"])
}
func TestApplyCodexOAuthTransform_CustomAndMCPToolOutputsPreserveCallID(t *testing.T) {
reqBody := map[string]any{
"model": "gpt-5.2",
"input": []any{
map[string]any{"type": "custom_tool_call_output", "call_id": "call_custom", "output": "ok"},
map[string]any{"type": "mcp_tool_call_output", "call_id": "call_mcp", "output": "ok"},
},
}
applyCodexOAuthTransform(reqBody, false, false)
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 2)
first, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "fccustom", first["call_id"])
second, ok := input[1].(map[string]any)
require.True(t, ok)
require.Equal(t, "fcmcp", second["call_id"])
}
func TestApplyCodexOAuthTransform_ImageAndWebSearchCallsDoNotGainCallID(t *testing.T) {
reqBody := map[string]any{
"model": "gpt-5.2",
"input": []any{
map[string]any{"type": "image_generation_call", "id": "ig_123", "status": "completed"},
map[string]any{"type": "web_search_call", "call_id": "call_bad", "status": "completed"},
},
"tool_choice": "auto",
}
applyCodexOAuthTransform(reqBody, false, false)
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 2)
first, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "ig_123", first["id"])
_, hasCallID := first["call_id"]
require.False(t, hasCallID)
second, ok := input[1].(map[string]any)
require.True(t, ok)
_, hasCallID = second["call_id"]
require.False(t, hasCallID)
}
func TestApplyCodexOAuthTransform_ExplicitStoreFalsePreserved(t *testing.T) { func TestApplyCodexOAuthTransform_ExplicitStoreFalsePreserved(t *testing.T) {
// 续链场景:显式 store=false 不再强制为 true保持 false。 // 续链场景:显式 store=false 不再强制为 true保持 false。

View File

@@ -21,7 +21,7 @@ type FunctionCallOutputValidation struct {
} }
// NeedsToolContinuation 判定请求是否需要工具调用续链处理。 // NeedsToolContinuation 判定请求是否需要工具调用续链处理。
// 满足以下任一信号即视为续链previous_response_id、input 内包含 function_call_output/item_reference、 // 满足以下任一信号即视为续链previous_response_id、input 内包含工具输出/item_reference、
// 或显式声明 tools/tool_choice。 // 或显式声明 tools/tool_choice。
func NeedsToolContinuation(reqBody map[string]any) bool { func NeedsToolContinuation(reqBody map[string]any) bool {
if reqBody == nil { if reqBody == nil {
@@ -46,7 +46,7 @@ func NeedsToolContinuation(reqBody map[string]any) bool {
continue continue
} }
itemType, _ := itemMap["type"].(string) itemType, _ := itemMap["type"].(string)
if itemType == "function_call_output" || itemType == "item_reference" { if isCodexToolCallItemType(itemType) || itemType == "item_reference" {
return true return true
} }
} }

View File

@@ -17,6 +17,9 @@ func TestNeedsToolContinuationSignals(t *testing.T) {
{name: "previous_response_id", body: map[string]any{"previous_response_id": "resp_1"}, want: true}, {name: "previous_response_id", body: map[string]any{"previous_response_id": "resp_1"}, want: true},
{name: "previous_response_id_blank", body: map[string]any{"previous_response_id": " "}, want: false}, {name: "previous_response_id_blank", body: map[string]any{"previous_response_id": " "}, want: false},
{name: "function_call_output", body: map[string]any{"input": []any{map[string]any{"type": "function_call_output"}}}, want: true}, {name: "function_call_output", body: map[string]any{"input": []any{map[string]any{"type": "function_call_output"}}}, want: true},
{name: "tool_search_output", body: map[string]any{"input": []any{map[string]any{"type": "tool_search_output"}}}, want: true},
{name: "custom_tool_call_output", body: map[string]any{"input": []any{map[string]any{"type": "custom_tool_call_output"}}}, want: true},
{name: "mcp_tool_call_output", body: map[string]any{"input": []any{map[string]any{"type": "mcp_tool_call_output"}}}, want: true},
{name: "item_reference", body: map[string]any{"input": []any{map[string]any{"type": "item_reference"}}}, want: true}, {name: "item_reference", body: map[string]any{"input": []any{map[string]any{"type": "item_reference"}}}, want: true},
{name: "tools", body: map[string]any{"tools": []any{map[string]any{"type": "function"}}}, want: true}, {name: "tools", body: map[string]any{"tools": []any{map[string]any{"type": "function"}}}, want: true},
{name: "tools_empty", body: map[string]any{"tools": []any{}}, want: false}, {name: "tools_empty", body: map[string]any{"tools": []any{}}, want: false},