diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index 14abde9b..65f7f5b4 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -658,12 +658,14 @@ func filterCodexInput(input []any, preserveReferences bool) []any { } } + if !isCodexToolCallItemType(typ) { + ensureCopy() + delete(newItem, "call_id") + } + if !preserveReferences { ensureCopy() delete(newItem, "id") - if !isCodexToolCallItemType(typ) { - delete(newItem, "call_id") - } } filtered = append(filtered, newItem) @@ -672,10 +674,20 @@ func filterCodexInput(input []any, preserveReferences bool) []any { } 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 strings.HasSuffix(typ, "_call") || strings.HasSuffix(typ, "_call_output") } func normalizeCodexTools(reqBody map[string]any) bool { diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 4fd16fdb..476f1ea9 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -92,6 +92,78 @@ func TestApplyCodexOAuthTransform_ToolContinuationNormalizesToolReferenceIDsOnly 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) { // 续链场景:显式 store=false 不再强制为 true,保持 false。 diff --git a/backend/internal/service/openai_tool_continuation.go b/backend/internal/service/openai_tool_continuation.go index dea3c172..c0f98de4 100644 --- a/backend/internal/service/openai_tool_continuation.go +++ b/backend/internal/service/openai_tool_continuation.go @@ -21,7 +21,7 @@ type FunctionCallOutputValidation struct { } // NeedsToolContinuation 判定请求是否需要工具调用续链处理。 -// 满足以下任一信号即视为续链:previous_response_id、input 内包含 function_call_output/item_reference、 +// 满足以下任一信号即视为续链:previous_response_id、input 内包含工具输出/item_reference、 // 或显式声明 tools/tool_choice。 func NeedsToolContinuation(reqBody map[string]any) bool { if reqBody == nil { @@ -46,7 +46,7 @@ func NeedsToolContinuation(reqBody map[string]any) bool { continue } itemType, _ := itemMap["type"].(string) - if itemType == "function_call_output" || itemType == "item_reference" { + if isCodexToolCallItemType(itemType) || itemType == "item_reference" { return true } } diff --git a/backend/internal/service/openai_tool_continuation_test.go b/backend/internal/service/openai_tool_continuation_test.go index fe737ad6..3f415d9d 100644 --- a/backend/internal/service/openai_tool_continuation_test.go +++ b/backend/internal/service/openai_tool_continuation_test.go @@ -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_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: "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: "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},