diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index 6535454403..11e34439cd 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -1,6 +1,8 @@ package service import ( + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "strings" @@ -83,6 +85,34 @@ type codexOAuthTransformOptions struct { PreserveToolCallIDs bool } +const ( + codexCallIDMaxLength = 64 + codexCallIDPrefix = "fc_" +) + +func normalizeCodexCallID(id string) string { + candidate := id + switch { + case id == "": + return "" + case strings.HasPrefix(id, "fc"): + case strings.HasPrefix(id, "call_"): + candidate = codexCallIDPrefix + strings.TrimPrefix(id, "call_") + default: + candidate = codexCallIDPrefix + id + } + if len(candidate) <= codexCallIDMaxLength { + return candidate + } + return compactCodexCallID(candidate) +} + +func compactCodexCallID(id string) string { + digest := sha256.Sum256([]byte("sub2api:codex-call-id:v1:" + id)) + encoded := hex.EncodeToString(digest[:]) + return codexCallIDPrefix + encoded[:codexCallIDMaxLength-len(codexCallIDPrefix)] +} + const codexImageGenerationFunctionToolName = "image_gen.imagegen" const ( @@ -1334,13 +1364,7 @@ func filterCodexInputWithOptions(input []any, opts codexInputFilterOptions) []an if opts.PreserveCallIDs { return id } - if id == "" || strings.HasPrefix(id, "fc") { - return id - } - if strings.HasPrefix(id, "call_") { - return "fc_" + strings.TrimPrefix(id, "call_") - } - return "fc_" + id + return normalizeCodexCallID(id) } if typ == "item_reference" { diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 40d0fd4b41..c184f49d7d 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -127,6 +127,70 @@ func TestApplyCodexOAuthTransform_ToolContinuationNormalizesToolReferenceIDsOnly require.Equal(t, "fc_1", second["call_id"]) } +func TestApplyCodexOAuthTransform_BoundsLongCallIDsAndPreservesPairing(t *testing.T) { + suffix := strings.Repeat("z", 62) + for _, tc := range []struct { + name string + callID string + outputCallID string + }{ + {name: "non-native boundary id", callID: "call-" + strings.Repeat("x", 59), outputCallID: "call-" + strings.Repeat("x", 59)}, + {name: "overlong fc id", callID: "fc_" + strings.Repeat("y", 62), outputCallID: "fc_" + strings.Repeat("y", 62)}, + {name: "equivalent prefixes", callID: "call_" + suffix, outputCallID: "fc_" + suffix}, + } { + t.Run(tc.name, func(t *testing.T) { + reqBody := map[string]any{ + "model": "gpt-5.2", + "input": []any{ + map[string]any{"type": "function_call", "call_id": tc.callID, "name": "shell"}, + map[string]any{"type": "function_call_output", "call_id": tc.outputCallID, "output": "done"}, + }, + } + + applyCodexOAuthTransform(reqBody, false, false) + + input, ok := reqBody["input"].([]any) + require.True(t, ok) + call, ok := input[0].(map[string]any) + require.True(t, ok) + output, ok := input[1].(map[string]any) + require.True(t, ok) + + fixedCallID, ok := call["call_id"].(string) + require.True(t, ok) + require.LessOrEqual(t, len(fixedCallID), codexCallIDMaxLength) + require.True(t, strings.HasPrefix(fixedCallID, codexCallIDPrefix)) + require.Equal(t, fixedCallID, output["call_id"]) + require.Equal(t, fixedCallID, normalizeCodexCallID(tc.callID)) + require.Equal(t, fixedCallID, normalizeCodexCallID(tc.outputCallID)) + }) + } +} + +func TestApplyCodexOAuthTransform_PreservesLongCallIDsWhenRequested(t *testing.T) { + callID := "call-" + strings.Repeat("x", 70) + reqBody := map[string]any{ + "model": "gpt-5.2", + "input": []any{ + map[string]any{"type": "function_call", "call_id": callID, "name": "shell"}, + map[string]any{"type": "function_call_output", "call_id": callID, "output": "done"}, + }, + } + + applyCodexOAuthTransformWithOptions(reqBody, codexOAuthTransformOptions{ + PreserveToolCallIDs: true, + }) + + input, ok := reqBody["input"].([]any) + require.True(t, ok) + call, ok := input[0].(map[string]any) + require.True(t, ok) + output, ok := input[1].(map[string]any) + require.True(t, ok) + require.Equal(t, callID, call["call_id"]) + require.Equal(t, callID, output["call_id"]) +} + func TestApplyCodexOAuthTransform_ToolSearchOutputPreservesCallID(t *testing.T) { reqBody := map[string]any{ "model": "gpt-5.2",