mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 04:50:52 +08:00
- Add SanitizeSchemaForGemini utility handling union types, allOf, exclusiveMinimum - Fix both gemini-cli and gemini API translators - Resolve "Proto field is not repeating, cannot start list" errors - Maintain backward compatibility with fallback logic This fixes Claude Code CLI compatibility issues when using tools with either Gemini CLI credentials or direct Gemini API keys by properly sanitizing JSON Schema fields that are incompatible with Gemini's Protocol Buffer validation.
196 lines
8.0 KiB
Go
196 lines
8.0 KiB
Go
// Package claude provides request translation functionality for Claude API.
|
|
// It handles parsing and transforming Claude API requests into the internal client format,
|
|
// extracting model information, system instructions, message contents, and tool declarations.
|
|
// The package also performs JSON data cleaning and transformation to ensure compatibility
|
|
// between Claude API format and the internal client's expected format.
|
|
package claude
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"strings"
|
|
|
|
client "github.com/luispater/CLIProxyAPI/v5/internal/interfaces"
|
|
"github.com/luispater/CLIProxyAPI/v5/internal/util"
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/sjson"
|
|
)
|
|
|
|
// ConvertClaudeRequestToGemini parses a Claude API request and returns a complete
|
|
// Gemini CLI request body (as JSON bytes) ready to be sent via SendRawMessageStream.
|
|
// All JSON transformations are performed using gjson/sjson.
|
|
//
|
|
// Parameters:
|
|
// - modelName: The name of the model.
|
|
// - rawJSON: The raw JSON request from the Claude API.
|
|
// - stream: A boolean indicating if the request is for a streaming response.
|
|
//
|
|
// Returns:
|
|
// - []byte: The transformed request in Gemini CLI format.
|
|
func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) []byte {
|
|
rawJSON := bytes.Clone(inputRawJSON)
|
|
var pathsToDelete []string
|
|
root := gjson.ParseBytes(rawJSON)
|
|
util.Walk(root, "", "additionalProperties", &pathsToDelete)
|
|
util.Walk(root, "", "$schema", &pathsToDelete)
|
|
|
|
var err error
|
|
for _, p := range pathsToDelete {
|
|
rawJSON, err = sjson.DeleteBytes(rawJSON, p)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
}
|
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
|
|
|
// system instruction
|
|
var systemInstruction *client.Content
|
|
systemResult := gjson.GetBytes(rawJSON, "system")
|
|
if systemResult.IsArray() {
|
|
systemResults := systemResult.Array()
|
|
systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
|
|
for i := 0; i < len(systemResults); i++ {
|
|
systemPromptResult := systemResults[i]
|
|
systemTypePromptResult := systemPromptResult.Get("type")
|
|
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
|
systemPrompt := systemPromptResult.Get("text").String()
|
|
systemPart := client.Part{Text: systemPrompt}
|
|
systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
|
|
}
|
|
}
|
|
if len(systemInstruction.Parts) == 0 {
|
|
systemInstruction = nil
|
|
}
|
|
}
|
|
|
|
// contents
|
|
contents := make([]client.Content, 0)
|
|
messagesResult := gjson.GetBytes(rawJSON, "messages")
|
|
if messagesResult.IsArray() {
|
|
messageResults := messagesResult.Array()
|
|
for i := 0; i < len(messageResults); i++ {
|
|
messageResult := messageResults[i]
|
|
roleResult := messageResult.Get("role")
|
|
if roleResult.Type != gjson.String {
|
|
continue
|
|
}
|
|
role := roleResult.String()
|
|
if role == "assistant" {
|
|
role = "model"
|
|
}
|
|
clientContent := client.Content{Role: role, Parts: []client.Part{}}
|
|
contentsResult := messageResult.Get("content")
|
|
if contentsResult.IsArray() {
|
|
contentResults := contentsResult.Array()
|
|
for j := 0; j < len(contentResults); j++ {
|
|
contentResult := contentResults[j]
|
|
contentTypeResult := contentResult.Get("type")
|
|
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
|
prompt := contentResult.Get("text").String()
|
|
clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
|
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
|
|
functionName := contentResult.Get("name").String()
|
|
functionArgs := contentResult.Get("input").String()
|
|
var args map[string]any
|
|
if err = json.Unmarshal([]byte(functionArgs), &args); err == nil {
|
|
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionCall: &client.FunctionCall{Name: functionName, Args: args}})
|
|
}
|
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
|
toolCallID := contentResult.Get("tool_use_id").String()
|
|
if toolCallID != "" {
|
|
funcName := toolCallID
|
|
toolCallIDs := strings.Split(toolCallID, "-")
|
|
if len(toolCallIDs) > 1 {
|
|
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
|
}
|
|
responseData := contentResult.Get("content").String()
|
|
functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
|
|
clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
|
|
}
|
|
}
|
|
}
|
|
contents = append(contents, clientContent)
|
|
} else if contentsResult.Type == gjson.String {
|
|
prompt := contentsResult.String()
|
|
contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
|
|
}
|
|
}
|
|
}
|
|
|
|
// tools
|
|
var tools []client.ToolDeclaration
|
|
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
|
if toolsResult.IsArray() {
|
|
tools = make([]client.ToolDeclaration, 1)
|
|
tools[0].FunctionDeclarations = make([]any, 0)
|
|
toolsResults := toolsResult.Array()
|
|
for i := 0; i < len(toolsResults); i++ {
|
|
toolResult := toolsResults[i]
|
|
inputSchemaResult := toolResult.Get("input_schema")
|
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
|
inputSchema := inputSchemaResult.Raw
|
|
// Use comprehensive schema sanitization for Gemini API compatibility
|
|
if sanitizedSchema, sanitizeErr := util.SanitizeSchemaForGemini(inputSchema); sanitizeErr == nil {
|
|
inputSchema = sanitizedSchema
|
|
} else {
|
|
// Fallback to basic cleanup if sanitization fails
|
|
inputSchema, _ = sjson.Delete(inputSchema, "additionalProperties")
|
|
inputSchema, _ = sjson.Delete(inputSchema, "$schema")
|
|
}
|
|
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
|
tool, _ = sjson.SetRaw(tool, "parameters", inputSchema)
|
|
var toolDeclaration any
|
|
if err = json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
|
|
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
tools = make([]client.ToolDeclaration, 0)
|
|
}
|
|
|
|
// Build output Gemini CLI request JSON
|
|
out := `{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}`
|
|
out, _ = sjson.Set(out, "model", modelName)
|
|
if systemInstruction != nil {
|
|
b, _ := json.Marshal(systemInstruction)
|
|
out, _ = sjson.SetRaw(out, "system_instruction", string(b))
|
|
}
|
|
if len(contents) > 0 {
|
|
b, _ := json.Marshal(contents)
|
|
out, _ = sjson.SetRaw(out, "contents", string(b))
|
|
}
|
|
if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
|
|
b, _ := json.Marshal(tools)
|
|
out, _ = sjson.SetRaw(out, "tools", string(b))
|
|
}
|
|
|
|
// Map reasoning and sampling configs
|
|
reasoningEffortResult := gjson.GetBytes(rawJSON, "reasoning_effort")
|
|
if reasoningEffortResult.String() == "none" {
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.include_thoughts", false)
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 0)
|
|
} else if reasoningEffortResult.String() == "auto" {
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
|
} else if reasoningEffortResult.String() == "low" {
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 1024)
|
|
} else if reasoningEffortResult.String() == "medium" {
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 8192)
|
|
} else if reasoningEffortResult.String() == "high" {
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", 24576)
|
|
} else {
|
|
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", -1)
|
|
}
|
|
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
|
out, _ = sjson.Set(out, "generationConfig.temperature", v.Num)
|
|
}
|
|
if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() && v.Type == gjson.Number {
|
|
out, _ = sjson.Set(out, "generationConfig.topP", v.Num)
|
|
}
|
|
if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() && v.Type == gjson.Number {
|
|
out, _ = sjson.Set(out, "generationConfig.topK", v.Num)
|
|
}
|
|
|
|
return []byte(out)
|
|
}
|