mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-18 20:30:51 +08:00
282 lines
10 KiB
Go
282 lines
10 KiB
Go
// 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_<alphanum>
|
|
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
|
|
}
|