// Package gemini provides request translation functionality for Gemini to Claude Code API compatibility. // It handles parsing and transforming Gemini API requests into Claude Code API format, // extracting model information, system instructions, message contents, and tool declarations. // The package performs JSON data transformation to ensure compatibility // between Gemini API format and Claude Code API's expected format. package gemini import ( "fmt" "strings" "github.com/router-for-me/CLIProxyAPI/v7/internal/registry" "github.com/router-for-me/CLIProxyAPI/v7/internal/thinking" translatorcommon "github.com/router-for-me/CLIProxyAPI/v7/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v7/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // ConvertGeminiRequestToClaude parses and transforms a Gemini API request into Claude Code 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 Claude Code API. // The function performs comprehensive transformation including: // 1. Model name mapping and generation configuration extraction // 2. System instruction conversion to Claude Code format // 3. Message content conversion with proper role mapping // 4. Tool call and tool result handling with FIFO queue for ID matching // 5. Image and file data conversion to Claude Code base64 format // 6. Tool declaration and tool choice configuration mapping // // Parameters: // - modelName: The name of the model to use for the request // - rawJSON: The raw JSON request data from the Gemini API // - stream: A boolean indicating if the request is for a streaming response // // Returns: // - []byte: The transformed request data in Claude Code API format func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := inputRawJSON userID := translatorcommon.DeriveClaudeUserID(rawJSON) // Base Claude message payload out := []byte(`{"model":"","max_tokens":32000,"messages":[],"metadata":{}}`) out, _ = sjson.SetBytes(out, "metadata.user_id", userID) root := gjson.ParseBytes(rawJSON) messageAccumulator := translatorcommon.NewClaudeMessageAccumulator(int(root.Get("contents.#").Int()) + 1) getGeminiToolID := func(value gjson.Result) string { if toolID := strings.TrimSpace(value.Get("id").String()); toolID != "" { return toolID } return strings.TrimSpace(value.Get("call_id").String()) } removePendingToolID := func(ids []string, toolID string) []string { if toolID == "" { return ids } for idx, pendingID := range ids { if pendingID == toolID { return append(ids[:idx], ids[idx+1:]...) } } return ids } // FIFO queue to store tool call IDs for matching with tool results // Gemini uses sequential pairing across possibly multiple in-flight // functionCalls, so we keep a FIFO queue of generated tool IDs and // consume them in order when functionResponses arrive. var pendingToolIDs []string toolCallCounter := 0 // Model mapping to specify which Claude Code model to use out, _ = sjson.SetBytes(out, "model", modelName) if serviceTier := root.Get("service_tier"); serviceTier.Exists() && serviceTier.Type == gjson.String { out, _ = sjson.SetBytes(out, "service_tier", serviceTier.String()) } // Generation config extraction from Gemini format if genConfig := root.Get("generationConfig"); genConfig.Exists() { // Max output tokens configuration if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() { out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } // Top P setting for nucleus sampling. if topP := genConfig.Get("topP"); topP.Exists() { out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Stop sequences configuration for custom termination conditions if stopSeqs := genConfig.Get("stopSequences"); stopSeqs.Exists() && stopSeqs.IsArray() { var stopSequences []string stopSeqs.ForEach(func(_, value gjson.Result) bool { stopSequences = append(stopSequences, value.String()) return true }) if len(stopSequences) > 0 { out, _ = sjson.SetBytes(out, "stop_sequences", stopSequences) } } // Include thoughts configuration for reasoning process visibility // Translator only does format conversion, ApplyThinking handles model capability validation. if thinkingConfig := genConfig.Get("thinkingConfig"); thinkingConfig.Exists() && thinkingConfig.IsObject() { 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)) // MapToClaudeEffort normalizes levels (e.g. minimal→low, xhigh→high) to avoid // validation errors since validate treats same-provider unsupported levels as errors. thinkingLevel := thinkingConfig.Get("thinkingLevel") if !thinkingLevel.Exists() { thinkingLevel = thinkingConfig.Get("thinking_level") } if thinkingLevel.Exists() { level := strings.ToLower(strings.TrimSpace(thinkingLevel.String())) if supportsAdaptive { switch level { case "": case "none": out, _ = sjson.SetBytes(out, "thinking.type", "disabled") 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 } out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.SetBytes(out, "output_config.effort", level) } } else { switch level { case "": case "none": out, _ = sjson.SetBytes(out, "thinking.type", "disabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") case "auto": out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") default: if budget, ok := thinking.ConvertLevelToBudget(level); ok { out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } } else { thinkingBudget := thinkingConfig.Get("thinkingBudget") if !thinkingBudget.Exists() { thinkingBudget = thinkingConfig.Get("thinking_budget") } if thinkingBudget.Exists() { budget := int(thinkingBudget.Int()) if supportsAdaptive { switch budget { case 0: out, _ = sjson.SetBytes(out, "thinking.type", "disabled") 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 } out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") out, _ = sjson.SetBytes(out, "output_config.effort", level) } } } else { switch budget { case 0: out, _ = sjson.SetBytes(out, "thinking.type", "disabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") case -1: out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") default: out, _ = sjson.SetBytes(out, "thinking.type", "enabled") out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } } } } // System instruction conversion to Claude Code format if sysInstr := root.Get("system_instruction"); sysInstr.Exists() { if parts := sysInstr.Get("parts"); parts.Exists() && parts.IsArray() { var systemText strings.Builder parts.ForEach(func(_, part gjson.Result) bool { if translatorcommon.IsGeminiThoughtPart(part) { return true } if text := part.Get("text"); text.Exists() { if systemText.Len() > 0 { systemText.WriteString("\n") } systemText.WriteString(text.String()) } return true }) if systemText.Len() > 0 { // Create system message in Claude Code format. systemMessage := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`) systemMessage, _ = sjson.SetBytes(systemMessage, "content.0.text", systemText.String()) messageAccumulator.Append(systemMessage) messageAccumulator.Flush() } } } // Contents conversion to messages with proper role mapping if contents := root.Get("contents"); contents.Exists() && contents.IsArray() { contents.ForEach(func(_, content gjson.Result) bool { role := content.Get("role").String() // Map Gemini roles to Claude Code roles if role == "model" { role = "assistant" } if role == "function" { role = "user" } if role == "tool" { role = "user" } contentItems := make([][]byte, 0, 4) if parts := content.Get("parts"); parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { if translatorcommon.IsGeminiThoughtPart(part) { return true } // Text content conversion if text := part.Get("text"); text.Exists() { textContent := []byte(`{"type":"text","text":""}`) textContent, _ = sjson.SetBytes(textContent, "text", text.String()) contentItems = append(contentItems, textContent) return true } // Function call (from model/assistant) conversion to tool use if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" { toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) // Reuse gateway-provided IDs when present, otherwise generate one for pairing. toolID := getGeminiToolID(fc) if toolID == "" { toolCallCounter++ toolID = fmt.Sprintf("toolu_gemini_%016d", toolCallCounter) } pendingToolIDs = append(pendingToolIDs, toolID) toolUse, _ = sjson.SetBytes(toolUse, "id", toolID) if name := fc.Get("name"); name.Exists() { toolUse, _ = sjson.SetBytes(toolUse, "name", name.String()) } if args := fc.Get("args"); args.Exists() && args.IsObject() { toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw)) } contentItems = append(contentItems, toolUse) return true } // Function response (from user) conversion to tool result if fr := part.Get("functionResponse"); fr.Exists() { toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`) // Attach the oldest queued tool_id to pair the response // with its call. If the queue is empty, generate a new id. var toolID string if customID := getGeminiToolID(fr); customID != "" { toolID = customID pendingToolIDs = removePendingToolID(pendingToolIDs, toolID) } else if len(pendingToolIDs) > 0 { toolID = pendingToolIDs[0] // Pop the first element from the queue pendingToolIDs = pendingToolIDs[1:] } else { // Fallback: generate new ID if no pending tool_use found toolCallCounter++ toolID = fmt.Sprintf("toolu_gemini_%016d", toolCallCounter) } toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", toolID) // Extract result content from the function response if result := fr.Get("response.result"); result.Exists() { toolResult, _ = sjson.SetBytes(toolResult, "content", result.String()) } else if response := fr.Get("response"); response.Exists() { toolResult, _ = sjson.SetBytes(toolResult, "content", response.Raw) } contentItems = append(contentItems, toolResult) return true } // Inline data conversion to Claude Code content format if inlineData := geminiClaudeInlineData(part); inlineData.Exists() { if contentPart, ok := claudeContentPartFromGeminiInlineData(inlineData); ok { contentItems = append(contentItems, contentPart) } return true } // File data conversion to Claude Code content format if fileData := geminiClaudeFileData(part); fileData.Exists() { if contentPart, ok := claudeContentPartFromGeminiFileData(fileData); ok { contentItems = append(contentItems, contentPart) } return true } return true }) } // Only add message if it has content. if len(contentItems) > 0 { msg := []byte(`{"role":"","content":[]}`) msg, _ = sjson.SetBytes(msg, "role", role) msg, _ = sjson.SetRawBytes(msg, "content", translatorcommon.JoinRawArray(contentItems)) messageAccumulator.Append(msg) } return true }) } out = translatorcommon.SetRawArrayItems(out, "messages", messageAccumulator.Messages()) // Tools mapping: Gemini functionDeclarations -> Claude Code tools if tools := root.Get("tools"); tools.Exists() && tools.IsArray() { var anthropicTools []interface{} tools.ForEach(func(_, tool gjson.Result) bool { if funcDecls := tool.Get("functionDeclarations"); funcDecls.Exists() && funcDecls.IsArray() { funcDecls.ForEach(func(_, funcDecl gjson.Result) bool { anthropicTool := []byte(`{"name":"","description":"","input_schema":{}}`) if name := funcDecl.Get("name"); name.Exists() { anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", name.String()) } if desc := funcDecl.Get("description"); desc.Exists() { anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", desc.String()) } if params := funcDecl.Get("parameters"); params.Exists() { cleaned := normalizeClaudeToolSchema(params) anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned) } else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() { cleaned := normalizeClaudeToolSchema(params) anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned) } anthropicTool = lowercaseClaudeToolSchemaTypes(anthropicTool) anthropicTools = append(anthropicTools, gjson.ParseBytes(anthropicTool).Value()) return true }) } return true }) if len(anthropicTools) > 0 { out, _ = sjson.SetBytes(out, "tools", anthropicTools) } } // Tool config mapping from Gemini format to Claude Code format if toolConfig := root.Get("tool_config"); toolConfig.Exists() { out = setClaudeToolChoiceFromGeminiToolConfig(out, toolConfig.Get("function_calling_config")) } else if toolConfig := root.Get("toolConfig"); toolConfig.Exists() { out = setClaudeToolChoiceFromGeminiToolConfig(out, toolConfig.Get("functionCallingConfig")) } // Stream setting configuration out, _ = sjson.SetBytes(out, "stream", stream) return out } func normalizeClaudeToolSchema(parameters gjson.Result) []byte { cleaned := []byte(parameters.Raw) if parameters.Get("additionalProperties").Type != gjson.False { cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false) } const schema = "http://json-schema.org/draft-07/schema#" currentSchema := parameters.Get("$schema") if currentSchema.Type != gjson.String || currentSchema.String() != schema { cleaned, _ = sjson.SetBytes(cleaned, "$schema", schema) } return cleaned } func lowercaseClaudeToolSchemaTypes(tool []byte) []byte { var pathsToLower []string util.Walk(gjson.ParseBytes(tool), "", "type", &pathsToLower) for _, path := range pathsToLower { typeValue := gjson.GetBytes(tool, path) normalizedType := strings.ToLower(typeValue.String()) if typeValue.Type == gjson.String && normalizedType == typeValue.String() { continue } tool, _ = sjson.SetBytes(tool, path, normalizedType) } return tool } func setClaudeToolChoiceFromGeminiToolConfig(out []byte, funcCalling gjson.Result) []byte { if !funcCalling.Exists() { return out } mode := funcCalling.Get("mode") if !mode.Exists() { return out } switch mode.String() { case "AUTO": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "NONE": out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"none"}`)) case "ANY": allowedNames := funcCalling.Get("allowedFunctionNames") if !allowedNames.Exists() { allowedNames = funcCalling.Get("allowed_function_names") } allowedNameItems := allowedNames.Array() if allowedNames.IsArray() && len(allowedNameItems) == 1 { choice := []byte(`{"type":"tool","name":""}`) choice, _ = sjson.SetBytes(choice, "name", allowedNameItems[0].String()) out, _ = sjson.SetRawBytes(out, "tool_choice", choice) } else { out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } } return out } func geminiClaudeInlineData(part gjson.Result) gjson.Result { inlineData := part.Get("inlineData") if inlineData.Exists() { return inlineData } return part.Get("inline_data") } func geminiClaudeFileData(part gjson.Result) gjson.Result { fileData := part.Get("fileData") if fileData.Exists() { return fileData } return part.Get("file_data") } func claudeContentPartFromGeminiInlineData(inlineData gjson.Result) ([]byte, bool) { mimeType := inlineData.Get("mimeType").String() if mimeType == "" { mimeType = inlineData.Get("mime_type").String() } data := inlineData.Get("data").String() if mimeType == "" || data == "" { return nil, false } lowerMimeType := strings.ToLower(mimeType) switch { case strings.HasPrefix(lowerMimeType, "image/"): imageContent := []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`) imageContent, _ = sjson.SetBytes(imageContent, "source.media_type", mimeType) imageContent, _ = sjson.SetBytes(imageContent, "source.data", data) return imageContent, true case strings.HasPrefix(lowerMimeType, "application/"), strings.HasPrefix(lowerMimeType, "text/"): documentContent := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`) documentContent, _ = sjson.SetBytes(documentContent, "source.media_type", mimeType) documentContent, _ = sjson.SetBytes(documentContent, "source.data", data) return documentContent, true default: return claudeTextContentPart(fmt.Sprintf("Media content: inline data (Type: %s)", mimeType)), true } } func claudeContentPartFromGeminiFileData(fileData gjson.Result) ([]byte, bool) { fileURI := fileData.Get("fileUri").String() if fileURI == "" { fileURI = fileData.Get("file_uri").String() } if fileURI == "" { return nil, false } mimeType := fileData.Get("mimeType").String() if mimeType == "" { mimeType = fileData.Get("mime_type").String() } lowerMimeType := strings.ToLower(mimeType) switch { case strings.HasPrefix(lowerMimeType, "image/"): imageContent := []byte(`{"type":"image","source":{"type":"url","url":""}}`) imageContent, _ = sjson.SetBytes(imageContent, "source.url", fileURI) return imageContent, true case strings.HasPrefix(lowerMimeType, "application/"), strings.HasPrefix(lowerMimeType, "text/"): documentContent := []byte(`{"type":"document","source":{"type":"url","url":""}}`) documentContent, _ = sjson.SetBytes(documentContent, "source.url", fileURI) if mimeType != "" { documentContent, _ = sjson.SetBytes(documentContent, "source.media_type", mimeType) } return documentContent, true default: fileInfo := "File: " + fileURI if mimeType != "" { fileInfo += " (Type: " + mimeType + ")" } return claudeTextContentPart(fileInfo), true } } func claudeTextContentPart(text string) []byte { textContent := []byte(`{"type":"text","text":""}`) textContent, _ = sjson.SetBytes(textContent, "text", text) return textContent }