// Package gemini provides request translation functionality for Gemini to Anthropic API. // It handles parsing and transforming Gemini API requests into Anthropic 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 Anthropic API's expected format. package gemini import ( "crypto/rand" "fmt" "math/big" "strings" "github.com/luispater/CLIProxyAPI/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) // ConvertGeminiRequestToAnthropic parses and transforms a Gemini API request into Anthropic 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 Anthropic API. func ConvertGeminiRequestToAnthropic(rawJSON []byte) string { // Base Anthropic API template out := `{"model":"","max_tokens":32000,"messages":[]}` root := gjson.ParseBytes(rawJSON) // Helper for generating tool call IDs in the form: toolu_ genToolCallID := func() string { const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" var b strings.Builder // 24 chars random suffix for i := 0; i < 24; i++ { n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) b.WriteByte(letters[n.Int64()]) } return "toolu_" + b.String() } // 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 // Model mapping if v := root.Get("model"); v.Exists() { modelName := v.String() out, _ = sjson.Set(out, "model", modelName) } // Generation config if genConfig := root.Get("generationConfig"); genConfig.Exists() { if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() { out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) } if temp := genConfig.Get("temperature"); temp.Exists() { out, _ = sjson.Set(out, "temperature", temp.Float()) } if topP := genConfig.Get("topP"); topP.Exists() { out, _ = sjson.Set(out, "top_p", topP.Float()) } 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.Set(out, "stop_sequences", stopSequences) } } } // System instruction -> system field 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 text := part.Get("text"); text.Exists() { if systemText.Len() > 0 { systemText.WriteString("\n") } systemText.WriteString(text.String()) } return true }) if systemText.Len() > 0 { systemMessage := `{"role":"user","content":[{"type":"text","text":""}]}` systemMessage, _ = sjson.Set(systemMessage, "content.0.text", systemText.String()) out, _ = sjson.SetRaw(out, "messages.-1", systemMessage) } } } // Contents -> messages if contents := root.Get("contents"); contents.Exists() && contents.IsArray() { contents.ForEach(func(_, content gjson.Result) bool { role := content.Get("role").String() if role == "model" { role = "assistant" } if role == "function" { role = "user" } // Create message msg := `{"role":"","content":[]}` msg, _ = sjson.Set(msg, "role", role) if parts := content.Get("parts"); parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { // Text content if text := part.Get("text"); text.Exists() { textContent := `{"type":"text","text":""}` textContent, _ = sjson.Set(textContent, "text", text.String()) msg, _ = sjson.SetRaw(msg, "content.-1", textContent) return true } // Function call (from model/assistant) if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" { toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` // Generate a unique tool ID and enqueue it for later matching // with the corresponding functionResponse toolID := genToolCallID() pendingToolIDs = append(pendingToolIDs, toolID) toolUse, _ = sjson.Set(toolUse, "id", toolID) if name := fc.Get("name"); name.Exists() { toolUse, _ = sjson.Set(toolUse, "name", name.String()) } if args := fc.Get("args"); args.Exists() { toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw) } msg, _ = sjson.SetRaw(msg, "content.-1", toolUse) return true } // Function response (from user) if fr := part.Get("functionResponse"); fr.Exists() { toolResult := `{"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 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 toolID = genToolCallID() } toolResult, _ = sjson.Set(toolResult, "tool_use_id", toolID) // Extract result content if result := fr.Get("response.result"); result.Exists() { toolResult, _ = sjson.Set(toolResult, "content", result.String()) } else if response := fr.Get("response"); response.Exists() { toolResult, _ = sjson.Set(toolResult, "content", response.Raw) } msg, _ = sjson.SetRaw(msg, "content.-1", toolResult) return true } // Image content (inline_data) if inlineData := part.Get("inline_data"); inlineData.Exists() { imageContent := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}` if mimeType := inlineData.Get("mime_type"); mimeType.Exists() { imageContent, _ = sjson.Set(imageContent, "source.media_type", mimeType.String()) } if data := inlineData.Get("data"); data.Exists() { imageContent, _ = sjson.Set(imageContent, "source.data", data.String()) } msg, _ = sjson.SetRaw(msg, "content.-1", imageContent) return true } // File data if fileData := part.Get("file_data"); fileData.Exists() { // For file data, we'll convert to text content with file info textContent := `{"type":"text","text":""}` fileInfo := "File: " + fileData.Get("file_uri").String() if mimeType := fileData.Get("mime_type"); mimeType.Exists() { fileInfo += " (Type: " + mimeType.String() + ")" } textContent, _ = sjson.Set(textContent, "text", fileInfo) msg, _ = sjson.SetRaw(msg, "content.-1", textContent) return true } return true }) } // Only add message if it has content if contentArray := gjson.Get(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 { out, _ = sjson.SetRaw(out, "messages.-1", msg) } return true }) } // Tools mapping: Gemini functionDeclarations -> Anthropic 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 := `"name":"","description":"","input_schema":{}}` if name := funcDecl.Get("name"); name.Exists() { anthropicTool, _ = sjson.Set(anthropicTool, "name", name.String()) } if desc := funcDecl.Get("description"); desc.Exists() { anthropicTool, _ = sjson.Set(anthropicTool, "description", desc.String()) } if params := funcDecl.Get("parameters"); params.Exists() { // Clean up the parameters schema cleaned := params.Raw cleaned, _ = sjson.Set(cleaned, "additionalProperties", false) cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#") anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned) } else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() { // Clean up the parameters schema cleaned := params.Raw cleaned, _ = sjson.Set(cleaned, "additionalProperties", false) cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#") anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned) } anthropicTools = append(anthropicTools, gjson.Parse(anthropicTool).Value()) return true }) } return true }) if len(anthropicTools) > 0 { out, _ = sjson.Set(out, "tools", anthropicTools) } } // Tool config if toolConfig := root.Get("tool_config"); toolConfig.Exists() { if funcCalling := toolConfig.Get("function_calling_config"); funcCalling.Exists() { if mode := funcCalling.Get("mode"); mode.Exists() { switch mode.String() { case "AUTO": out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"}) case "NONE": out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "none"}) case "ANY": out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"}) } } } } // Stream setting if stream := root.Get("stream"); stream.Exists() { out, _ = sjson.Set(out, "stream", stream.Bool()) } else { out, _ = sjson.Set(out, "stream", false) } var pathsToLower []string toolsResult := gjson.Get(out, "tools") util.Walk(toolsResult, "", "type", &pathsToLower) for _, p := range pathsToLower { fullPath := fmt.Sprintf("tools.%s", p) out, _ = sjson.Set(out, fullPath, strings.ToLower(gjson.Get(out, fullPath).String())) } return out }