Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions internal/translator/claude/gemini/claude_gemini_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,16 @@ import (
//
// Returns:
// - []byte: The transformed request data in Claude Code API format
func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte {
func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) ([]byte, error) {
return convertGeminiRequestToClaude(modelName, inputRawJSON, stream)

}

// convertGeminiRequestToClaude also reports a user turn that was left empty
// because its only attachment has no Claude equivalent.
func convertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) ([]byte, error) {
rawJSON := inputRawJSON
var drops translatorcommon.UserTurnDrops

userID := translatorcommon.DeriveClaudeUserID(rawJSON)

Expand Down Expand Up @@ -106,6 +114,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
mi := registry.LookupModelInfo(modelName, "claude")
supportsAdaptive := mi != nil && mi.Thinking != nil && len(mi.Thinking.Levels) > 0
supportsMax := supportsAdaptive && thinking.HasLevel(mi.Thinking.Levels, string(thinking.LevelMax))
supportsDynamic := supportsAdaptive && mi.Thinking.DynamicAllowed

// MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid
// validation errors since validate treats same-provider unsupported levels as errors.
Expand All @@ -116,15 +125,21 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if thinkingLevel.Exists() {
level := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
if supportsAdaptive {
switch level {
case "":
case "none":
switch {
case level == "":
case level == "none":
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
out, _ = sjson.DeleteBytes(out, "output_config.effort")
case level == "auto" && supportsDynamic:
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
out, _ = sjson.DeleteBytes(out, "output_config.effort")
default:
if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok {
level = mapped
if !thinking.HasLevel(mi.Thinking.Levels, level) {
if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok {
level = mapped
}
}
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
Expand Down Expand Up @@ -154,16 +169,22 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if thinkingBudget.Exists() {
budget := int(thinkingBudget.Int())
if supportsAdaptive {
switch budget {
case 0:
switch {
case budget == 0:
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
out, _ = sjson.DeleteBytes(out, "output_config.effort")
case budget == -1 && supportsDynamic:
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
out, _ = sjson.DeleteBytes(out, "output_config.effort")
default:
level, ok := thinking.ConvertBudgetToLevel(budget)
if ok {
if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM {
level = mapped
if !thinking.HasLevel(mi.Thinking.Levels, level) {
if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM {
level = mapped
}
}
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
Expand All @@ -189,7 +210,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
}

// System instruction conversion to Claude Code format
if sysInstr := root.Get("system_instruction"); sysInstr.Exists() {
sysInstr := root.Get("systemInstruction")
if !sysInstr.Exists() {
sysInstr = root.Get("system_instruction")
}
if sysInstr.Exists() {
if parts := sysInstr.Get("parts"); parts.Exists() && parts.IsArray() {
var systemText strings.Builder
parts.ForEach(func(_, part gjson.Result) bool {
Expand Down Expand Up @@ -232,6 +257,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
}

contentItems := make([][]byte, 0, 4)
// Counts what this turn sends; whitespace-only text is forwarded but never sendable.
sendable := 0
if parts := content.Get("parts"); parts.Exists() && parts.IsArray() {
parts.ForEach(func(_, part gjson.Result) bool {
if translatorcommon.IsGeminiThoughtPart(part) {
Expand All @@ -243,6 +270,9 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
textContent := []byte(`{"type":"text","text":""}`)
textContent, _ = sjson.SetBytes(textContent, "text", text.String())
contentItems = append(contentItems, textContent)
if strings.TrimSpace(text.String()) != "" {
sendable++
}
return true
}

Expand All @@ -266,6 +296,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw))
}
contentItems = append(contentItems, toolUse)
sendable++
return true
}

Expand Down Expand Up @@ -297,13 +328,19 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
toolResult, _ = sjson.SetBytes(toolResult, "content", response.Raw)
}
contentItems = append(contentItems, toolResult)
sendable++
return true
}

// Inline data conversion to Claude Code content format
// Inline data conversion to Claude Code content format. Only model content
// keeps a placeholder for media Claude cannot read; a user attachment is
// never replaced by text, so it is recorded as dropped instead.
if inlineData := geminiClaudeInlineData(part); inlineData.Exists() {
if contentPart, ok := claudeContentPartFromGeminiInlineData(inlineData); ok {
if contentPart, ok := claudeContentPartFromGeminiInlineData(inlineData, role == "assistant"); ok {
contentItems = append(contentItems, contentPart)
sendable++
} else if role != "assistant" {
drops.Drop("inlineData")
}
return true
}
Expand All @@ -312,13 +349,19 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if fileData := geminiClaudeFileData(part); fileData.Exists() {
if contentPart, ok := claudeContentPartFromGeminiFileData(fileData); ok {
contentItems = append(contentItems, contentPart)
sendable++
} else if role != "assistant" {
drops.Drop("fileData")
}
return true
}

return true
})
}
if role != "assistant" {
drops.EndTurn(sendable)
}

// Only add message if it has content.
if len(contentItems) > 0 {
Expand Down Expand Up @@ -381,7 +424,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
// Stream setting configuration
out, _ = sjson.SetBytes(out, "stream", stream)

return thinking.ApplyTranslatedSummaryToClaude(out, rawJSON, "gemini", modelName)
return thinking.ApplyTranslatedSummaryToClaude(out, rawJSON, "gemini", modelName), drops.Err()
}

func normalizeClaudeToolSchema(parameters gjson.Result) []byte {
Expand Down Expand Up @@ -457,7 +500,10 @@ func geminiClaudeFileData(part gjson.Result) gjson.Result {
return part.Get("file_data")
}

func claudeContentPartFromGeminiInlineData(inlineData gjson.Result) ([]byte, bool) {
// claudeContentPartFromGeminiInlineData maps inline bytes onto a Claude block.
// Media that Claude cannot read becomes placeholder text only when
// keepPlaceholder is set; otherwise it reports false so the caller can refuse it.
func claudeContentPartFromGeminiInlineData(inlineData gjson.Result, keepPlaceholder bool) ([]byte, bool) {
mimeType := inlineData.Get("mimeType").String()
if mimeType == "" {
mimeType = inlineData.Get("mime_type").String()
Expand All @@ -479,6 +525,9 @@ func claudeContentPartFromGeminiInlineData(inlineData gjson.Result) ([]byte, boo
documentContent, _ = sjson.SetBytes(documentContent, "source.data", data)
return documentContent, true
default:
if !keepPlaceholder {
return nil, false
}
return claudeTextContentPart(fmt.Sprintf("Media content: inline data (Type: %s)", mimeType)), true
}
}
Expand Down
Loading
Loading