From 89771216a14c2a0d7f8e4ed8f9f7dee0b2c0ee19 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Wed, 19 Nov 2025 11:34:13 +0800 Subject: [PATCH] **feat(translator): add ThoughtSignature handling in Gemini request transformations** --- internal/interfaces/client_models.go | 3 +++ .../gemini-cli/claude/gemini-cli_claude_request.go | 7 ++++++- .../openai/chat-completions/gemini-cli_openai_request.go | 3 +++ internal/translator/gemini/claude/gemini_claude_request.go | 7 ++++++- .../openai/chat-completions/gemini_openai_request.go | 3 +++ .../openai/responses/gemini_openai-responses_request.go | 3 +++ 6 files changed, 24 insertions(+), 2 deletions(-) diff --git a/internal/interfaces/client_models.go b/internal/interfaces/client_models.go index a9ce59a0..885b471a 100644 --- a/internal/interfaces/client_models.go +++ b/internal/interfaces/client_models.go @@ -62,6 +62,9 @@ type Part struct { // InlineData contains base64-encoded data with its MIME type (e.g., images). InlineData *InlineData `json:"inlineData,omitempty"` + // ThoughtSignature is a provider-required signature that accompanies certain parts. + ThoughtSignature string `json:"thoughtSignature,omitempty"` + // FunctionCall represents a tool call requested by the model. FunctionCall *FunctionCall `json:"functionCall,omitempty"` diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go index 1081125c..50fd5a25 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -17,6 +17,8 @@ import ( "github.com/tidwall/sjson" ) +const geminiCLIClaudeThoughtSignature = "skip_thought_signature_validator" + // ConvertClaudeRequestToCLI parses and transforms a Claude Code API request into Gemini CLI API format. // It extracts the model name, system instruction, message contents, and tool declarations // from the raw JSON request and returns them in the format expected by the Gemini CLI API. @@ -89,7 +91,10 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] functionArgs := contentResult.Get("input").String() var args map[string]any if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { - clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}}) + clientContent.Parts = append(clientContent.Parts, client.Part{ + FunctionCall: &client.FunctionCall{Name: functionName, Args: args}, + ThoughtSignature: geminiCLIClaudeThoughtSignature, + }) } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { toolCallID := contentResult.Get("tool_use_id").String() diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index e009f86b..6eb467c6 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -15,6 +15,8 @@ import ( "github.com/tidwall/sjson" ) +const geminiCLIFunctionThoughtSignature = "skip_thought_signature_validator" + // ConvertOpenAIRequestToGeminiCLI converts an OpenAI Chat Completions request (raw JSON) // into a complete Gemini CLI request JSON. All JSON construction uses sjson and lookups use gjson. // @@ -239,6 +241,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo fargs := tc.Get("function.arguments").String() node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature) p++ if fid != "" { fIDs = append(fIDs, fid) diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 2f0976d3..05f9be5d 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -17,6 +17,8 @@ import ( "github.com/tidwall/sjson" ) +const geminiClaudeThoughtSignature = "skip_thought_signature_validator" + // ConvertClaudeRequestToGemini parses a Claude API request and returns a complete // Gemini CLI request body (as JSON bytes) ready to be sent via SendRawMessageStream. // All JSON transformations are performed using gjson/sjson. @@ -82,7 +84,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) functionArgs := contentResult.Get("input").String() var args map[string]any if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { - clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}}) + clientContent.Parts = append(clientContent.Parts, client.Part{ + FunctionCall: &client.FunctionCall{Name: functionName, Args: args}, + ThoughtSignature: geminiClaudeThoughtSignature, + }) } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { toolCallID := contentResult.Get("tool_use_id").String() diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index 71009d25..f2860e9c 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -15,6 +15,8 @@ import ( "github.com/tidwall/sjson" ) +const geminiFunctionThoughtSignature = "skip_thought_signature_validator" + // ConvertOpenAIRequestToGemini converts an OpenAI Chat Completions request (raw JSON) // into a complete Gemini request JSON. All JSON construction uses sjson and lookups use gjson. // @@ -264,6 +266,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) fargs := tc.Get("function.arguments").String() node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) + node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature) p++ if fid != "" { fIDs = append(fIDs, fid) diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 40606339..4eeebf3c 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -10,6 +10,8 @@ import ( "github.com/tidwall/sjson" ) +const geminiResponsesThoughtSignature = "skip_thought_signature_validator" + func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := bytes.Clone(inputRawJSON) @@ -108,6 +110,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte modelContent := `{"role":"model","parts":[]}` functionCall := `{"functionCall":{"name":"","args":{}}}` functionCall, _ = sjson.Set(functionCall, "functionCall.name", name) + functionCall, _ = sjson.Set(functionCall, "thoughtSignature", geminiResponsesThoughtSignature) // Parse arguments JSON string and set as args object if arguments != "" {