mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-03 13:00: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.
203 lines
8.7 KiB
Go
203 lines
8.7 KiB
Go
// Package claude provides request translation functionality for Claude Code API compatibility.
|
|
// This package handles the conversion of Claude Code API requests into Gemini CLI-compatible
|
|
// JSON format, transforming message contents, system instructions, and tool declarations
|
|
// into the format expected by Gemini CLI API clients. It performs JSON data transformation
|
|
// to ensure compatibility between Claude Code API format and Gemini CLI API'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"
|
|
)
|
|
|
|
// ConvertClaudeRequestToCLI parses and transforms a Claude Code API request into Gemini CLI 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 Gemini CLI API.
|
|
// The function performs the following transformations:
|
|
// 1. Extracts the model information from the request
|
|
// 2. Restructures the JSON to match Gemini CLI API format
|
|
// 3. Converts system instructions to the expected format
|
|
// 4. Maps message contents with proper role transformations
|
|
// 5. Handles tool declarations and tool choices
|
|
// 6. Maps generation configuration parameters
|
|
//
|
|
// Parameters:
|
|
// - modelName: The name of the model to use for the request
|
|
// - rawJSON: The raw JSON request data from the Claude Code API
|
|
// - stream: A boolean indicating if the request is for a streaming response (unused in current implementation)
|
|
//
|
|
// Returns:
|
|
// - []byte: The transformed request data in Gemini CLI API format
|
|
func ConvertClaudeRequestToCLI(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 := `{"model":"","request":{"contents":[],"generationConfig":{"thinkingConfig":{"include_thoughts":true}}}}`
|
|
out, _ = sjson.Set(out, "model", modelName)
|
|
if systemInstruction != nil {
|
|
b, _ := json.Marshal(systemInstruction)
|
|
out, _ = sjson.SetRaw(out, "request.systemInstruction", string(b))
|
|
}
|
|
if len(contents) > 0 {
|
|
b, _ := json.Marshal(contents)
|
|
out, _ = sjson.SetRaw(out, "request.contents", string(b))
|
|
}
|
|
if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
|
|
b, _ := json.Marshal(tools)
|
|
out, _ = sjson.SetRaw(out, "request.tools", string(b))
|
|
}
|
|
|
|
// Map reasoning and sampling configs
|
|
reasoningEffortResult := gjson.GetBytes(rawJSON, "reasoning_effort")
|
|
if reasoningEffortResult.String() == "none" {
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.include_thoughts", false)
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 0)
|
|
} else if reasoningEffortResult.String() == "auto" {
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
|
} else if reasoningEffortResult.String() == "low" {
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 1024)
|
|
} else if reasoningEffortResult.String() == "medium" {
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 8192)
|
|
} else if reasoningEffortResult.String() == "high" {
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", 24576)
|
|
} else {
|
|
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
|
}
|
|
if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number {
|
|
out, _ = sjson.Set(out, "request.generationConfig.temperature", v.Num)
|
|
}
|
|
if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() && v.Type == gjson.Number {
|
|
out, _ = sjson.Set(out, "request.generationConfig.topP", v.Num)
|
|
}
|
|
if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() && v.Type == gjson.Number {
|
|
out, _ = sjson.Set(out, "request.generationConfig.topK", v.Num)
|
|
}
|
|
|
|
return []byte(out)
|
|
}
|