**feat(translator): add ThoughtSignature handling in Gemini request transformations**

This commit is contained in:
Luis Pater
2025-11-19 11:34:13 +08:00
parent 14ddfd4b79
commit 89771216a1
6 changed files with 24 additions and 2 deletions

View File

@@ -62,6 +62,9 @@ type Part struct {
// InlineData contains base64-encoded data with its MIME type (e.g., images). // InlineData contains base64-encoded data with its MIME type (e.g., images).
InlineData *InlineData `json:"inlineData,omitempty"` 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 represents a tool call requested by the model.
FunctionCall *FunctionCall `json:"functionCall,omitempty"` FunctionCall *FunctionCall `json:"functionCall,omitempty"`

View File

@@ -17,6 +17,8 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
const geminiCLIClaudeThoughtSignature = "skip_thought_signature_validator"
// ConvertClaudeRequestToCLI parses and transforms a Claude Code API request into Gemini CLI API format. // 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 // 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. // 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() functionArgs := contentResult.Get("input").String()
var args map[string]any var args map[string]any
if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { 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" { } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
toolCallID := contentResult.Get("tool_use_id").String() toolCallID := contentResult.Get("tool_use_id").String()

View File

@@ -15,6 +15,8 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
const geminiCLIFunctionThoughtSignature = "skip_thought_signature_validator"
// ConvertOpenAIRequestToGeminiCLI converts an OpenAI Chat Completions request (raw JSON) // 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. // 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() fargs := tc.Get("function.arguments").String()
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
p++ p++
if fid != "" { if fid != "" {
fIDs = append(fIDs, fid) fIDs = append(fIDs, fid)

View File

@@ -17,6 +17,8 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
const geminiClaudeThoughtSignature = "skip_thought_signature_validator"
// ConvertClaudeRequestToGemini parses a Claude API request and returns a complete // ConvertClaudeRequestToGemini parses a Claude API request and returns a complete
// Gemini CLI request body (as JSON bytes) ready to be sent via SendRawMessageStream. // Gemini CLI request body (as JSON bytes) ready to be sent via SendRawMessageStream.
// All JSON transformations are performed using gjson/sjson. // All JSON transformations are performed using gjson/sjson.
@@ -82,7 +84,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
functionArgs := contentResult.Get("input").String() functionArgs := contentResult.Get("input").String()
var args map[string]any var args map[string]any
if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { 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" { } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
toolCallID := contentResult.Get("tool_use_id").String() toolCallID := contentResult.Get("tool_use_id").String()

View File

@@ -15,6 +15,8 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
const geminiFunctionThoughtSignature = "skip_thought_signature_validator"
// ConvertOpenAIRequestToGemini converts an OpenAI Chat Completions request (raw JSON) // ConvertOpenAIRequestToGemini converts an OpenAI Chat Completions request (raw JSON)
// into a complete Gemini request JSON. All JSON construction uses sjson and lookups use gjson. // 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() fargs := tc.Get("function.arguments").String()
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname) node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs)) node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiFunctionThoughtSignature)
p++ p++
if fid != "" { if fid != "" {
fIDs = append(fIDs, fid) fIDs = append(fIDs, fid)

View File

@@ -10,6 +10,8 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
) )
const geminiResponsesThoughtSignature = "skip_thought_signature_validator"
func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte, stream bool) []byte { func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte, stream bool) []byte {
rawJSON := bytes.Clone(inputRawJSON) rawJSON := bytes.Clone(inputRawJSON)
@@ -108,6 +110,7 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
modelContent := `{"role":"model","parts":[]}` modelContent := `{"role":"model","parts":[]}`
functionCall := `{"functionCall":{"name":"","args":{}}}` functionCall := `{"functionCall":{"name":"","args":{}}}`
functionCall, _ = sjson.Set(functionCall, "functionCall.name", name) functionCall, _ = sjson.Set(functionCall, "functionCall.name", name)
functionCall, _ = sjson.Set(functionCall, "thoughtSignature", geminiResponsesThoughtSignature)
// Parse arguments JSON string and set as args object // Parse arguments JSON string and set as args object
if arguments != "" { if arguments != "" {