fix(translator): 上游流式 tool_calls 缺少 id 时合成 call_id 保证事件链完整

部分 OpenAI 兼容上游(自建 vLLM/sglang、部分中转网关)在流式 delta 中
不携带 tool_calls[].id。原逻辑要求非空 id 才发出
response.output_item.added/done 事件链,导致 Codex 侧完全收不到
函数调用(表现为工具全部不可用)。非流式路径同样产出 call_id 为空
的残缺 item。

缺失 id 时按 call_<responseID>_<choiceIdx>_<toolIndex> 合成确定性
call_id;chat completions 上游无状态,call_id 仅需单请求内自洽,
合成 id 可安全往返。

Ref #4219
This commit is contained in:
辰ing
2026-07-12 20:39:54 +08:00
committed by Luis Pater
parent 0ba5fab529
commit 07455ecba7
@@ -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":""}`)