From 07455ecba76da31bf98544103c345b60d176db1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=B0ing?= Date: Sat, 11 Jul 2026 18:06:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(translator):=20=E4=B8=8A=E6=B8=B8=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=20tool=5Fcalls=20=E7=BC=BA=E5=B0=91=20id=20=E6=97=B6?= =?UTF-8?q?=E5=90=88=E6=88=90=20call=5Fid=20=E4=BF=9D=E8=AF=81=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E9=93=BE=E5=AE=8C=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 部分 OpenAI 兼容上游(自建 vLLM/sglang、部分中转网关)在流式 delta 中 不携带 tool_calls[].id。原逻辑要求非空 id 才发出 response.output_item.added/done 事件链,导致 Codex 侧完全收不到 函数调用(表现为工具全部不可用)。非流式路径同样产出 call_id 为空 的残缺 item。 缺失 id 时按 call___ 合成确定性 call_id;chat completions 上游无状态,call_id 仅需单请求内自洽, 合成 id 可安全往返。 Ref #4219 --- .../responses/openai_openai-responses_response.go | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go index 6f9e2a5e..fcaaa638 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -479,7 +479,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, existingCallID := st.FuncCallIDs[key] effectiveCallID := existingCallID shouldEmitItem := false - if existingCallID == "" && newCallID != "" { + if existingCallID == "" { + if newCallID == "" { + // Some OpenAI-compatible providers omit tool_call ids in + // streaming deltas; synthesize one so the Responses event + // chain (output_item.added/done) still fires for Codex. + newCallID = fmt.Sprintf("call_%s_%d_%d", st.ResponseID, idx, toolIndex) + } effectiveCallID = newCallID st.FuncCallIDs[key] = newCallID st.FuncOutputIx[key] = allocOutputIndex() @@ -758,8 +764,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co // Function/tool calls if tcs := msg.Get("tool_calls"); tcs.Exists() && tcs.IsArray() { - tcs.ForEach(func(_, tc gjson.Result) bool { + tcs.ForEach(func(tcIndex, tc gjson.Result) bool { callID := tc.Get("id").String() + if callID == "" { + // Providers may omit tool_call ids; synthesize one so the + // function_call item stays usable for Codex round-trips. + callID = fmt.Sprintf("call_%s_%d_%d", id, choice.Get("index").Int(), tcIndex.Int()) + } name := tc.Get("function.name").String() args := tc.Get("function.arguments").String() item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)