diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index a3b69dee5fd..23babc6a297 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -109,8 +109,6 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact func applyCodexOAuthTransformWithOptions(reqBody map[string]any, opts codexOAuthTransformOptions) codexTransformResult { result := codexTransformResult{} - // 工具续链需求会影响存储策略与 input 过滤逻辑。 - needsToolContinuation := NeedsToolContinuation(reqBody) model := "" if v, ok := reqBody["model"].(string); ok { @@ -226,8 +224,24 @@ func applyCodexOAuthTransformWithOptions(reqBody map[string]any, opts codexOAuth input = normalizedInput result.Modified = true } + // PreserveReferences is intentionally false on the OAuth path: this + // transform forces store=false on the upstream request (see line ~140 + // above, plus normalizeOpenAIPassthroughOAuthBody), and ChatGPT's + // internal /v1/responses backend rejects any item_reference (or any + // item carrying an `id` from a prior turn) with 404 "Items are not + // persisted when `store` is set to false." Continuation context still + // works end-to-end because cumora-style clients inline both the + // function_call and matching function_call_output items in the same + // input, which is the standard Responses API continuation shape. + // + // Background: 70eaa450 ("修复工具续链校验与存储策略") flipped this to + // `needsToolContinuation` on the assumption it could also force + // store=true on continuation turns. 3663951d reverted the store=true + // half the next day after upstream returned "Store must be set to + // false", but left the PreserveReferences flip in place — leaving + // every continuation request guaranteed to 404. input = filterCodexInputWithOptions(input, codexInputFilterOptions{ - PreserveReferences: needsToolContinuation, + PreserveReferences: false, PreserveCallIDs: opts.PreserveToolCallIDs, }) reqBody["input"] = input diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 9c72760aa2b..33f4bf99be6 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -8,8 +8,11 @@ import ( "github.com/stretchr/testify/require" ) -func TestApplyCodexOAuthTransform_ToolContinuationPreservesInput(t *testing.T) { - // 续链场景:保留 item_reference 与 id,但不再强制 store=true。 +func TestApplyCodexOAuthTransform_ToolContinuationDropsItemReferenceAndStripsIDs(t *testing.T) { + // OAuth 路径强制 store=false,任何指向"已存储 item"的引用 (item_reference + // 整项 / 其它 item 上的 id 字段) 上游都会 404 "Items are not persisted when + // `store` is set to false."。续链上下文靠 input 内 inline 的 function_call + + // function_call_output (匹配 call_id) 承载,已经足够。 reqBody := map[string]any{ "model": "gpt-5.2", @@ -22,26 +25,19 @@ func TestApplyCodexOAuthTransform_ToolContinuationPreservesInput(t *testing.T) { applyCodexOAuthTransform(reqBody, false, false) - // 未显式设置 store=true,默认为 false。 store, ok := reqBody["store"].(bool) require.True(t, ok) require.False(t, store) input, ok := reqBody["input"].([]any) require.True(t, ok) - require.Len(t, input, 2) + require.Len(t, input, 1, "item_reference must be dropped on OAuth path") - // 校验 input[0] 为 map,避免断言失败导致测试中断。 - first, ok := input[0].(map[string]any) + only, ok := input[0].(map[string]any) require.True(t, ok) - require.Equal(t, "item_reference", first["type"]) - require.Equal(t, "ref1", first["id"]) - - // 校验 input[1] 为 map,确保后续字段断言安全。 - second, ok := input[1].(map[string]any) - require.True(t, ok) - require.Equal(t, "o1", second["id"]) - require.Equal(t, "fc1", second["call_id"]) + require.Equal(t, "function_call_output", only["type"]) + require.Equal(t, "fc_1", only["call_id"]) + require.NotContains(t, only, "id", "stored-item id must be stripped on OAuth path") } func TestApplyCodexOAuthTransform_MessagesBridgePromptCacheKeyIsHeaderOnly(t *testing.T) { @@ -77,7 +73,10 @@ func TestApplyCodexOAuthTransform_MessagesBridgePromptCacheKeyIsHeaderOnly(t *te require.NotContains(t, reqBody, "prompt_cache_key") } -func TestApplyCodexOAuthTransform_ToolContinuationPreservesNativeMessageAndReasoningIDs(t *testing.T) { +func TestApplyCodexOAuthTransform_DropsItemReferenceAndStripsMessageID(t *testing.T) { + // 即便 item_reference.id 是 rs_* (reasoning) 或者 message 上挂 id,store=false + // 路径下上游同样无法解析任何"先前已存储" item。除了 item_reference 必须丢弃, + // 其它 item 上的 id 字段也得剥掉。 reqBody := map[string]any{ "model": "gpt-5.2", "input": []any{ @@ -91,18 +90,17 @@ func TestApplyCodexOAuthTransform_ToolContinuationPreservesNativeMessageAndReaso input, ok := reqBody["input"].([]any) require.True(t, ok) - require.Len(t, input, 2) + require.Len(t, input, 1, "item_reference must be dropped") first, ok := input[0].(map[string]any) require.True(t, ok) - require.Equal(t, "msg_0", first["id"]) - - second, ok := input[1].(map[string]any) - require.True(t, ok) - require.Equal(t, "rs_123", second["id"]) + require.Equal(t, "message", first["type"]) + require.NotContains(t, first, "id", "stored-item id must be stripped") } -func TestApplyCodexOAuthTransform_ToolContinuationNormalizesToolReferenceIDsOnly(t *testing.T) { +func TestApplyCodexOAuthTransform_DropsItemReferenceButKeepsCallIDOnOutput(t *testing.T) { + // item_reference 整项丢弃;function_call_output 保留并按需把 call_* → fc_*。 + // 续链上下文由保留下来的 function_call_output (匹配 call_id) 承载。 reqBody := map[string]any{ "model": "gpt-5.2", "input": []any{ @@ -116,15 +114,12 @@ func TestApplyCodexOAuthTransform_ToolContinuationNormalizesToolReferenceIDsOnly input, ok := reqBody["input"].([]any) require.True(t, ok) - require.Len(t, input, 2) + require.Len(t, input, 1, "item_reference must be dropped") - first, ok := input[0].(map[string]any) - require.True(t, ok) - require.Equal(t, "fc1", first["id"]) - - second, ok := input[1].(map[string]any) + only, ok := input[0].(map[string]any) require.True(t, ok) - require.Equal(t, "fc1", second["call_id"]) + require.Equal(t, "function_call_output", only["type"]) + require.Equal(t, "fc_1", only["call_id"]) } func TestApplyCodexOAuthTransform_ToolSearchOutputPreservesCallID(t *testing.T) {