diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go index e069f7ec..24694e1d 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "strings" + "sync/atomic" "time" . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" @@ -24,6 +25,9 @@ type convertCliResponseToOpenAIChatParams struct { FunctionIndex int } +// functionCallIDCounter provides a process-wide unique counter for function call identifiers. +var functionCallIDCounter uint64 + // ConvertAntigravityResponseToOpenAI translates a single chunk of a streaming response from the // Gemini CLI API format to the OpenAI Chat Completions streaming format. // It processes various Gemini CLI event types and transforms them into OpenAI-compatible JSON responses. @@ -146,7 +150,7 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d", fcName, time.Now().UnixNano())) + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go index 733668f3..9b37c52b 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "strings" + "sync/atomic" "time" "github.com/tidwall/gjson" @@ -27,6 +28,9 @@ type Params struct { ResponseIndex int // Index counter for content blocks in the streaming response } +// toolUseIDCounter provides a process-wide unique counter for tool use identifiers. +var toolUseIDCounter uint64 + // ConvertGeminiCLIResponseToClaude performs sophisticated streaming response format conversion. // This function implements a complex state machine that translates backend client responses // into Claude Code-compatible Server-Sent Events (SSE) format. It manages different response types @@ -197,7 +201,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // Create the tool use block with unique ID and function details data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex) - data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d", fcName, time.Now().UnixNano())) + data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1))) data, _ = sjson.Set(data, "content_block.name", fcName) output = output + fmt.Sprintf("data: %s\n\n\n", data) diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go index 9c422a07..753870f3 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "strings" + "sync/atomic" "time" . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/openai/chat-completions" @@ -24,6 +25,9 @@ type convertCliResponseToOpenAIChatParams struct { FunctionIndex int } +// functionCallIDCounter provides a process-wide unique counter for function call identifiers. +var functionCallIDCounter uint64 + // ConvertCliResponseToOpenAI translates a single chunk of a streaming response from the // Gemini CLI API format to the OpenAI Chat Completions streaming format. // It processes various Gemini CLI event types and transforms them into OpenAI-compatible JSON responses. @@ -146,7 +150,7 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d", fcName, time.Now().UnixNano())) + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { diff --git a/internal/translator/gemini/claude/gemini_claude_response.go b/internal/translator/gemini/claude/gemini_claude_response.go index a80171a9..8fd566df 100644 --- a/internal/translator/gemini/claude/gemini_claude_response.go +++ b/internal/translator/gemini/claude/gemini_claude_response.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "strings" + "sync/atomic" "time" "github.com/tidwall/gjson" @@ -26,6 +27,9 @@ type Params struct { ResponseIndex int } +// toolUseIDCounter provides a process-wide unique counter for tool use identifiers. +var toolUseIDCounter uint64 + // ConvertGeminiResponseToClaude performs sophisticated streaming response format conversion. // This function implements a complex state machine that translates backend client responses // into Claude-compatible Server-Sent Events (SSE) format. It manages different response types @@ -197,7 +201,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // Create the tool use block with unique ID and function details data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex) - data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d", fcName, time.Now().UnixNano())) + data, _ = sjson.Set(data, "content_block.id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1))) data, _ = sjson.Set(data, "content_block.name", fcName) output = output + fmt.Sprintf("data: %s\n\n\n", data) diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go index 12e28cca..a1ebc855 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go @@ -11,6 +11,7 @@ import ( "encoding/json" "fmt" "strings" + "sync/atomic" "time" "github.com/tidwall/gjson" @@ -23,6 +24,9 @@ type convertGeminiResponseToOpenAIChatParams struct { FunctionIndex int } +// functionCallIDCounter provides a process-wide unique counter for function call identifiers. +var functionCallIDCounter uint64 + // ConvertGeminiResponseToOpenAI translates a single chunk of a streaming response from the // Gemini API format to the OpenAI Chat Completions streaming format. // It processes various Gemini event types and transforms them into OpenAI-compatible JSON responses. @@ -148,7 +152,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d", fcName, time.Now().UnixNano())) + functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { @@ -281,7 +285,7 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina } functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}` fcName := functionCallResult.Get("name").String() - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d", fcName, time.Now().UnixNano())) + functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fcArgsResult.Raw) diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go index ce221863..e08b265d 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "strings" + "sync/atomic" "time" "github.com/tidwall/gjson" @@ -37,6 +38,12 @@ type geminiToResponsesState struct { FuncCallIDs map[int]string } +// responseIDCounter provides a process-wide unique counter for synthesized response identifiers. +var responseIDCounter uint64 + +// funcCallIDCounter provides a process-wide unique counter for function call identifiers. +var funcCallIDCounter uint64 + func emitEvent(event string, payload string) string { return fmt.Sprintf("event: %s\ndata: %s", event, payload) } @@ -205,7 +212,7 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, st.FuncArgsBuf[idx] = &strings.Builder{} } if st.FuncCallIDs[idx] == "" { - st.FuncCallIDs[idx] = fmt.Sprintf("call_%d", time.Now().UnixNano()) + st.FuncCallIDs[idx] = fmt.Sprintf("call_%d_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1)) } st.FuncNames[idx] = name @@ -464,7 +471,7 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string // id: prefer provider responseId, otherwise synthesize id := root.Get("responseId").String() if id == "" { - id = fmt.Sprintf("resp_%x", time.Now().UnixNano()) + id = fmt.Sprintf("resp_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&responseIDCounter, 1)) } // Normalize to response-style id (prefix resp_ if missing) if !strings.HasPrefix(id, "resp_") { @@ -575,7 +582,7 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string if fc := p.Get("functionCall"); fc.Exists() { name := fc.Get("name").String() args := fc.Get("args") - callID := fmt.Sprintf("call_%x", time.Now().UnixNano()) + callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1)) outputs = append(outputs, map[string]interface{}{ "id": fmt.Sprintf("fc_%s", callID), "type": "function_call", 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 00ec5c7f..c698b93f 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "strings" + "sync/atomic" "time" "github.com/tidwall/gjson" @@ -41,6 +42,9 @@ type oaiToResponsesState struct { UsageSeen bool } +// responseIDCounter provides a process-wide unique counter for synthesized response identifiers. +var responseIDCounter uint64 + func emitRespEvent(event string, payload string) string { return fmt.Sprintf("event: %s\ndata: %s", event, payload) } @@ -590,7 +594,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co // id: use provider id if present, otherwise synthesize id := root.Get("id").String() if id == "" { - id = fmt.Sprintf("resp_%x", time.Now().UnixNano()) + id = fmt.Sprintf("resp_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&responseIDCounter, 1)) } resp, _ = sjson.Set(resp, "id", id)