Merge pull request #1910 from slovx2/fix/codex-tool-call-ids
fix(openai): 修复 Codex 工具调用 call_id 处理
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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。
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
Reference in New Issue
Block a user