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":""}`)