fix(openai): preserve 路径 call_id 超长时压缩至 64 字符上限

生产环境真实报错:客户端走 /v1/messages(Anthropic Messages 格式)入站,
OAuth 账号转发上游 OpenAI /v1/responses 时(PreserveToolCallIDs=true),
客户端历史中出现超长工具 ID(如嵌套代理拼接产生的 78 字符 srvtoolu_ ID)
会被原样透传,上游 400 拒绝:
"Invalid 'input[614].call_id': string too long. Expected a string with
maximum length 64, but got a string with length 78 instead."

#4612 为非 preserve 路径增加了 call_id 归一化与压缩,但有意将
PreserveToolCallIDs=true 的路径排除在外。本次在 preserve 路径补上长度护栏:
- len(id) <= 64 时仍原样透传,保持 #4612 承诺的"保持原样"语义;
- 超过 64 时复用已有的 compactCodexCallID 做确定性 SHA-256 压缩。

由于压缩是确定性的,同一逻辑 ID 在 function_call 与 function_call_output
两侧结果一致,tool_use/tool_result 配对不破坏;OAuth 路径 store=false
每轮全量重放历史,跨轮结果亦一致。改写点 fixCallIDPrefix 是
filterCodexInputWithOptions 内所有 call_id 修正的单一入口(含 item_reference
的 call_* 引用),同时覆盖 Messages 桥接(openai_gateway_messages.go)与
/v1/responses 透传(openai_gateway_forward.go)两条 preserve 路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
superman2003andCursor committed 2026-07-20 18:49:24 +08:00
1 parent e625ce3b3b
commit c401595e01
2 files changed
+58 -5

No files matched your search

@@ -1409,7 +1409,15 @@ func filterCodexInputWithOptions(input []any, opts codexInputFilterOptions) []an
// 若 item_reference 指向 legacy call_* 标识,则仅修正该引用本身。
fixCallIDPrefix := func(id string) string {
if opts.PreserveCallIDs {
return id
// preserve 模式尽量原样透传客户端 id 以维持 tool_use/tool_result
// 配对,但上游对 call_id 有 64 字符硬上限,超长原样透传必然被
// 400 拒绝("Invalid 'input[N].call_id': string too long")。
// 超长时退回确定性压缩:同一逻辑 id 在 function_call 与
// function_call_output 两侧结果一致,配对不受影响。
if len(id) <= codexCallIDMaxLength {
return id
}
return compactCodexCallID(id)
}
return normalizeCodexCallID(id)
}
@@ -167,8 +167,48 @@ func TestApplyCodexOAuthTransform_BoundsLongCallIDsAndPreservesPairing(t *testin
}
}
func TestApplyCodexOAuthTransform_PreservesLongCallIDsWhenRequested(t *testing.T) {
callID := "call-" + strings.Repeat("x", 70)
func TestApplyCodexOAuthTransform_PreservesCallIDsWithinLimitWhenRequested(t *testing.T) {
// preserve 模式下 ≤64 字符的 id 必须原样透传(含 64 字符等长边界),
// 不做任何前缀改写或压缩。
for _, tc := range []struct {
name string
callID string
}{
{name: "anthropic toolu id", callID: "toolu_01ABCdefGHIjklMNOpqrsTUV"},
{name: "boundary 64 chars", callID: "toolu_" + strings.Repeat("x", codexCallIDMaxLength-len("toolu_"))},
} {
t.Run(tc.name, func(t *testing.T) {
require.LessOrEqual(t, len(tc.callID), codexCallIDMaxLength)
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.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, tc.callID, call["call_id"])
require.Equal(t, tc.callID, output["call_id"])
})
}
}
func TestApplyCodexOAuthTransform_CompactsOverlongCallIDsWhenPreserveRequested(t *testing.T) {
// preserve 模式下超过 64 字符的 id 若原样透传,上游必然 400
// ("Invalid 'input[N].call_id': string too long"),需退回确定性压缩;
// function_call 与 function_call_output 两侧压缩结果一致,配对保持。
callID := "srvtoolu_" + strings.Repeat("x", 69) // 78 字符,对应生产环境真实报错长度
require.Len(t, callID, 78)
reqBody := map[string]any{
"model": "gpt-5.2",
"input": []any{
@@ -187,8 +227,13 @@ func TestApplyCodexOAuthTransform_PreservesLongCallIDsWhenRequested(t *testing.T
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"])
compacted, ok := call["call_id"].(string)
require.True(t, ok)
require.Len(t, compacted, codexCallIDMaxLength)
require.True(t, strings.HasPrefix(compacted, codexCallIDPrefix))
require.Equal(t, compacted, output["call_id"], "两侧压缩结果必须一致以保持配对")
require.Equal(t, compactCodexCallID(callID), compacted, "压缩必须是确定性的")
}
func TestApplyCodexOAuthTransform_ToolSearchOutputPreservesCallID(t *testing.T) {