mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
Refactor codebase
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// Package geminiCLI provides request translation functionality for Gemini CLI to Claude Code API compatibility.
|
||||
// It handles parsing and transforming Gemini CLI 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 CLI API format and Claude Code API's expected format.
|
||||
package geminiCLI
|
||||
|
||||
import (
|
||||
. "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertGeminiCLIRequestToClaude parses and transforms a Gemini CLI 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 the following transformations:
|
||||
// 1. Extracts the model information from the request
|
||||
// 2. Restructures the JSON to match Claude Code API format
|
||||
// 3. Converts system instructions to the expected format
|
||||
// 4. Delegates to the Gemini-to-Claude conversion function for further processing
|
||||
//
|
||||
// Parameters:
|
||||
// - modelName: The name of the model to use for the request
|
||||
// - rawJSON: The raw JSON request data from the Gemini CLI 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 ConvertGeminiCLIRequestToClaude(modelName string, rawJSON []byte, stream bool) []byte {
|
||||
modelResult := gjson.GetBytes(rawJSON, "model")
|
||||
// Extract the inner request object and promote it to the top level
|
||||
rawJSON = []byte(gjson.GetBytes(rawJSON, "request").Raw)
|
||||
// Restore the model information at the top level
|
||||
rawJSON, _ = sjson.SetBytes(rawJSON, "model", modelResult.String())
|
||||
// Convert systemInstruction field to system_instruction for Claude Code compatibility
|
||||
if gjson.GetBytes(rawJSON, "systemInstruction").Exists() {
|
||||
rawJSON, _ = sjson.SetRawBytes(rawJSON, "system_instruction", []byte(gjson.GetBytes(rawJSON, "systemInstruction").Raw))
|
||||
rawJSON, _ = sjson.DeleteBytes(rawJSON, "systemInstruction")
|
||||
}
|
||||
// Delegate to the Gemini-to-Claude conversion function for further processing
|
||||
return ConvertGeminiRequestToClaude(modelName, rawJSON, stream)
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Package geminiCLI provides response translation functionality for Claude Code to Gemini CLI API compatibility.
|
||||
// This package handles the conversion of Claude Code API responses into Gemini CLI-compatible
|
||||
// JSON format, transforming streaming events and non-streaming responses into the format
|
||||
// expected by Gemini CLI API clients.
|
||||
package geminiCLI
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/luispater/CLIProxyAPI/internal/translator/claude/gemini"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertClaudeResponseToGeminiCLI converts Claude Code streaming response format to Gemini CLI format.
|
||||
// This function processes various Claude Code event types and transforms them into Gemini-compatible JSON responses.
|
||||
// It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini CLI API format.
|
||||
// The function wraps each converted response in a "response" object to match the Gemini CLI API structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response
|
||||
// - rawJSON: The raw JSON response from the Claude Code API
|
||||
// - param: A pointer to a parameter object for maintaining state between calls
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object
|
||||
func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, rawJSON []byte, param *any) []string {
|
||||
outputs := ConvertClaudeResponseToGemini(ctx, modelName, rawJSON, param)
|
||||
// Wrap each converted response in a "response" object to match Gemini CLI API structure
|
||||
newOutputs := make([]string, 0)
|
||||
for i := 0; i < len(outputs); i++ {
|
||||
json := `{"response": {}}`
|
||||
output, _ := sjson.SetRaw(json, "response", outputs[i])
|
||||
newOutputs = append(newOutputs, output)
|
||||
}
|
||||
return newOutputs
|
||||
}
|
||||
|
||||
// ConvertClaudeResponseToGeminiCLINonStream converts a non-streaming Claude Code response to a non-streaming Gemini CLI response.
|
||||
// This function processes the complete Claude Code response and transforms it into a single Gemini-compatible
|
||||
// JSON response. It wraps the converted response in a "response" object to match the Gemini CLI API structure.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response
|
||||
// - rawJSON: The raw JSON response from the Claude Code API
|
||||
// - param: A pointer to a parameter object for the conversion
|
||||
//
|
||||
// Returns:
|
||||
// - string: A Gemini-compatible JSON response wrapped in a response object
|
||||
func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName string, rawJSON []byte, param *any) string {
|
||||
strJSON := ConvertClaudeResponseToGeminiNonStream(ctx, modelName, rawJSON, param)
|
||||
// Wrap the converted response in a "response" object to match Gemini CLI API structure
|
||||
json := `{"response": {}}`
|
||||
strJSON, _ = sjson.SetRaw(json, "response", strJSON)
|
||||
return strJSON
|
||||
|
||||
}
|
||||
19
internal/translator/claude/gemini-cli/init.go
Normal file
19
internal/translator/claude/gemini-cli/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package geminiCLI
|
||||
|
||||
import (
|
||||
. "github.com/luispater/CLIProxyAPI/internal/constant"
|
||||
"github.com/luispater/CLIProxyAPI/internal/interfaces"
|
||||
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
GEMINICLI,
|
||||
CLAUDE,
|
||||
ConvertGeminiCLIRequestToClaude,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertClaudeResponseToGeminiCLI,
|
||||
NonStream: ConvertClaudeResponseToGeminiCLINonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Package gemini provides request translation functionality for Gemini to Anthropic API.
|
||||
// It handles parsing and transforming Gemini API requests into Anthropic API format,
|
||||
// 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 Anthropic API's expected format.
|
||||
// between Gemini API format and Claude Code API's expected format.
|
||||
package gemini
|
||||
|
||||
import (
|
||||
@@ -16,20 +16,36 @@ import (
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertGeminiRequestToAnthropic parses and transforms a Gemini API request into Anthropic API format.
|
||||
// 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 Anthropic API.
|
||||
func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
// Base Anthropic API template
|
||||
// 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, rawJSON []byte, stream bool) []byte {
|
||||
// Base Claude Code API template with default max_tokens value
|
||||
out := `{"model":"","max_tokens":32000,"messages":[]}`
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Helper for generating tool call IDs in the form: toolu_<alphanum>
|
||||
// This ensures unique identifiers for tool calls in the Claude Code format
|
||||
genToolCallID := func() string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var b strings.Builder
|
||||
// 24 chars random suffix
|
||||
// 24 chars random suffix for uniqueness
|
||||
for i := 0; i < 24; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
b.WriteByte(letters[n.Int64()])
|
||||
@@ -43,23 +59,24 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
// 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)
|
||||
}
|
||||
// Model mapping to specify which Claude Code model to use
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
|
||||
// Generation config
|
||||
// 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.Set(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
// Temperature setting for controlling response randomness
|
||||
if temp := genConfig.Get("temperature"); temp.Exists() {
|
||||
out, _ = sjson.Set(out, "temperature", temp.Float())
|
||||
}
|
||||
// Top P setting for nucleus sampling
|
||||
if topP := genConfig.Get("topP"); topP.Exists() {
|
||||
out, _ = sjson.Set(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 {
|
||||
@@ -72,7 +89,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// System instruction -> system field
|
||||
// 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
|
||||
@@ -86,6 +103,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
return true
|
||||
})
|
||||
if systemText.Len() > 0 {
|
||||
// Create system message in Claude Code format
|
||||
systemMessage := `{"role":"user","content":[{"type":"text","text":""}]}`
|
||||
systemMessage, _ = sjson.Set(systemMessage, "content.0.text", systemText.String())
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", systemMessage)
|
||||
@@ -93,10 +111,11 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Contents -> messages
|
||||
// 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"
|
||||
}
|
||||
@@ -105,13 +124,17 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
// Create message
|
||||
if role == "tool" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
// Create message structure in Claude Code format
|
||||
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
|
||||
// Text content conversion
|
||||
if text := part.Get("text"); text.Exists() {
|
||||
textContent := `{"type":"text","text":""}`
|
||||
textContent, _ = sjson.Set(textContent, "text", text.String())
|
||||
@@ -119,7 +142,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
return true
|
||||
}
|
||||
|
||||
// Function call (from model/assistant)
|
||||
// Function call (from model/assistant) conversion to tool use
|
||||
if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" {
|
||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||
|
||||
@@ -139,7 +162,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
return true
|
||||
}
|
||||
|
||||
// Function response (from user)
|
||||
// Function response (from user) conversion to tool result
|
||||
if fr := part.Get("functionResponse"); fr.Exists() {
|
||||
toolResult := `{"type":"tool_result","tool_use_id":"","content":""}`
|
||||
|
||||
@@ -156,7 +179,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
toolResult, _ = sjson.Set(toolResult, "tool_use_id", toolID)
|
||||
|
||||
// Extract result content
|
||||
// Extract result content from the function response
|
||||
if result := fr.Get("response.result"); result.Exists() {
|
||||
toolResult, _ = sjson.Set(toolResult, "content", result.String())
|
||||
} else if response := fr.Get("response"); response.Exists() {
|
||||
@@ -166,7 +189,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
return true
|
||||
}
|
||||
|
||||
// Image content (inline_data)
|
||||
// Image content (inline_data) conversion to Claude Code format
|
||||
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() {
|
||||
@@ -179,7 +202,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
return true
|
||||
}
|
||||
|
||||
// File data
|
||||
// File data conversion to text content with file info
|
||||
if fileData := part.Get("file_data"); fileData.Exists() {
|
||||
// For file data, we'll convert to text content with file info
|
||||
textContent := `{"type":"text","text":""}`
|
||||
@@ -205,14 +228,14 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
})
|
||||
}
|
||||
|
||||
// Tools mapping: Gemini functionDeclarations -> Anthropic tools
|
||||
// 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 := `"name":"","description":"","input_schema":{}}`
|
||||
anthropicTool := `{"name":"","description":"","input_schema":{}}`
|
||||
|
||||
if name := funcDecl.Get("name"); name.Exists() {
|
||||
anthropicTool, _ = sjson.Set(anthropicTool, "name", name.String())
|
||||
@@ -221,13 +244,13 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
anthropicTool, _ = sjson.Set(anthropicTool, "description", desc.String())
|
||||
}
|
||||
if params := funcDecl.Get("parameters"); params.Exists() {
|
||||
// Clean up the parameters schema
|
||||
// Clean up the parameters schema for Claude Code compatibility
|
||||
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
|
||||
// Clean up the parameters schema for Claude Code compatibility
|
||||
cleaned := params.Raw
|
||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
||||
cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
||||
@@ -246,7 +269,7 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Tool config
|
||||
// Tool config mapping from Gemini format to Claude Code format
|
||||
if toolConfig := root.Get("tool_config"); toolConfig.Exists() {
|
||||
if funcCalling := toolConfig.Get("function_calling_config"); funcCalling.Exists() {
|
||||
if mode := funcCalling.Get("mode"); mode.Exists() {
|
||||
@@ -262,13 +285,10 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Stream setting
|
||||
if stream := root.Get("stream"); stream.Exists() {
|
||||
out, _ = sjson.Set(out, "stream", stream.Bool())
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "stream", false)
|
||||
}
|
||||
// Stream setting configuration
|
||||
out, _ = sjson.Set(out, "stream", stream)
|
||||
|
||||
// Convert tool parameter types to lowercase for Claude Code compatibility
|
||||
var pathsToLower []string
|
||||
toolsResult := gjson.Get(out, "tools")
|
||||
util.Walk(toolsResult, "", "type", &pathsToLower)
|
||||
@@ -277,5 +297,5 @@ func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
out, _ = sjson.Set(out, fullPath, strings.ToLower(gjson.Get(out, fullPath).String()))
|
||||
}
|
||||
|
||||
return out
|
||||
return []byte(out)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// Package gemini provides response translation functionality for Anthropic to Gemini API.
|
||||
// This package handles the conversion of Anthropic API responses into Gemini-compatible
|
||||
// Package gemini provides response translation functionality for Claude Code to Gemini API compatibility.
|
||||
// This package handles the conversion of Claude Code API responses into Gemini-compatible
|
||||
// JSON format, transforming streaming events and non-streaming responses into the format
|
||||
// expected by Gemini API clients. It supports both streaming and non-streaming modes,
|
||||
// handling text content, tool calls, and usage metadata appropriately.
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -13,8 +16,15 @@ import (
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
var (
|
||||
dataTag = []byte("data: ")
|
||||
)
|
||||
|
||||
// ConvertAnthropicResponseToGeminiParams holds parameters for response conversion
|
||||
// It also carries minimal streaming state across calls to assemble tool_use input_json_delta.
|
||||
// This structure maintains state information needed for proper conversion of streaming responses
|
||||
// from Claude Code format to Gemini format, particularly for handling tool calls that span
|
||||
// multiple streaming events.
|
||||
type ConvertAnthropicResponseToGeminiParams struct {
|
||||
Model string
|
||||
CreatedAt int64
|
||||
@@ -28,74 +38,96 @@ type ConvertAnthropicResponseToGeminiParams struct {
|
||||
ToolUseArgs map[int]*strings.Builder // accumulates partial_json across deltas
|
||||
}
|
||||
|
||||
// ConvertAnthropicResponseToGemini converts Anthropic streaming response format to Gemini format.
|
||||
// This function processes various Anthropic event types and transforms them into Gemini-compatible JSON responses.
|
||||
// It handles text content, tool calls, and usage metadata, outputting responses that match the Gemini API format.
|
||||
func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicResponseToGeminiParams) []string {
|
||||
// ConvertClaudeResponseToGemini converts Claude Code streaming response format to Gemini format.
|
||||
// This function processes various Claude Code event types and transforms them into Gemini-compatible JSON responses.
|
||||
// It handles text content, tool calls, reasoning content, and usage metadata, outputting responses that match
|
||||
// the Gemini API format. The function supports incremental updates for streaming responses and maintains
|
||||
// state information to properly assemble multi-part tool calls.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response
|
||||
// - rawJSON: The raw JSON response from the Claude Code API
|
||||
// - param: A pointer to a parameter object for maintaining state between calls
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response
|
||||
func ConvertClaudeResponseToGemini(_ context.Context, modelName string, rawJSON []byte, param *any) []string {
|
||||
if *param == nil {
|
||||
*param = &ConvertAnthropicResponseToGeminiParams{
|
||||
Model: modelName,
|
||||
CreatedAt: 0,
|
||||
ResponseID: "",
|
||||
}
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||
return []string{}
|
||||
}
|
||||
rawJSON = rawJSON[6:]
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
// Base Gemini response template
|
||||
// Base Gemini response template with default values
|
||||
template := `{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
||||
|
||||
// Set model version
|
||||
if param.Model != "" {
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).Model != "" {
|
||||
// Map Claude model names back to Gemini model names
|
||||
template, _ = sjson.Set(template, "modelVersion", param.Model)
|
||||
template, _ = sjson.Set(template, "modelVersion", (*param).(*ConvertAnthropicResponseToGeminiParams).Model)
|
||||
}
|
||||
|
||||
// Set response ID and creation time
|
||||
if param.ResponseID != "" {
|
||||
template, _ = sjson.Set(template, "responseId", param.ResponseID)
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID != "" {
|
||||
template, _ = sjson.Set(template, "responseId", (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID)
|
||||
}
|
||||
|
||||
// Set creation time to current time if not provided
|
||||
if param.CreatedAt == 0 {
|
||||
param.CreatedAt = time.Now().Unix()
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt == 0 {
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt = time.Now().Unix()
|
||||
}
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix(param.CreatedAt, 0).Format(time.RFC3339Nano))
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix((*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano))
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Initialize response with message metadata
|
||||
// Initialize response with message metadata when a new message begins
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
param.ResponseID = message.Get("id").String()
|
||||
param.Model = message.Get("model").String()
|
||||
template, _ = sjson.Set(template, "responseId", param.ResponseID)
|
||||
template, _ = sjson.Set(template, "modelVersion", param.Model)
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID = message.Get("id").String()
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).Model = message.Get("model").String()
|
||||
}
|
||||
return []string{template}
|
||||
return []string{}
|
||||
|
||||
case "content_block_start":
|
||||
// Start of a content block - record tool_use name by index for functionCall
|
||||
// Start of a content block - record tool_use name by index for functionCall assembly
|
||||
if cb := root.Get("content_block"); cb.Exists() {
|
||||
if cb.Get("type").String() == "tool_use" {
|
||||
idx := int(root.Get("index").Int())
|
||||
if param.ToolUseNames == nil {
|
||||
param.ToolUseNames = map[int]string{}
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames == nil {
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames = map[int]string{}
|
||||
}
|
||||
if name := cb.Get("name"); name.Exists() {
|
||||
param.ToolUseNames[idx] = name.String()
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames[idx] = name.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
return []string{}
|
||||
|
||||
case "content_block_delta":
|
||||
// Handle content delta (text, thinking, or tool use)
|
||||
// Handle content delta (text, thinking, or tool use arguments)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
// Regular text content delta
|
||||
// Regular text content delta for normal response text
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
textPart := `{"text":""}`
|
||||
textPart, _ = sjson.Set(textPart, "text", text.String())
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", textPart)
|
||||
}
|
||||
case "thinking_delta":
|
||||
// Thinking/reasoning content delta
|
||||
// Thinking/reasoning content delta for models with reasoning capabilities
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
thinkingPart := `{"thought":true,"text":""}`
|
||||
thinkingPart, _ = sjson.Set(thinkingPart, "text", text.String())
|
||||
@@ -104,13 +136,13 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
case "input_json_delta":
|
||||
// Tool use input delta - accumulate partial_json by index for later assembly at content_block_stop
|
||||
idx := int(root.Get("index").Int())
|
||||
if param.ToolUseArgs == nil {
|
||||
param.ToolUseArgs = map[int]*strings.Builder{}
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs == nil {
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs = map[int]*strings.Builder{}
|
||||
}
|
||||
b, ok := param.ToolUseArgs[idx]
|
||||
b, ok := (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs[idx]
|
||||
if !ok || b == nil {
|
||||
bb := &strings.Builder{}
|
||||
param.ToolUseArgs[idx] = bb
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs[idx] = bb
|
||||
b = bb
|
||||
}
|
||||
if pj := delta.Get("partial_json"); pj.Exists() {
|
||||
@@ -127,12 +159,12 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
// Claude's content_block_stop often doesn't include content_block payload (see docs/response-claude.txt)
|
||||
// So we finalize using accumulated state captured during content_block_start and input_json_delta.
|
||||
name := ""
|
||||
if param.ToolUseNames != nil {
|
||||
name = param.ToolUseNames[idx]
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames != nil {
|
||||
name = (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames[idx]
|
||||
}
|
||||
var argsTrim string
|
||||
if param.ToolUseArgs != nil {
|
||||
if b := param.ToolUseArgs[idx]; b != nil {
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs != nil {
|
||||
if b := (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs[idx]; b != nil {
|
||||
argsTrim = strings.TrimSpace(b.String())
|
||||
}
|
||||
}
|
||||
@@ -146,20 +178,20 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
}
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", functionCall)
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
param.LastStorageOutput = template
|
||||
(*param).(*ConvertAnthropicResponseToGeminiParams).LastStorageOutput = template
|
||||
// cleanup used state for this index
|
||||
if param.ToolUseArgs != nil {
|
||||
delete(param.ToolUseArgs, idx)
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs != nil {
|
||||
delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs, idx)
|
||||
}
|
||||
if param.ToolUseNames != nil {
|
||||
delete(param.ToolUseNames, idx)
|
||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames != nil {
|
||||
delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames, idx)
|
||||
}
|
||||
return []string{template}
|
||||
}
|
||||
return []string{}
|
||||
|
||||
case "message_delta":
|
||||
// Handle message-level changes (like stop reason)
|
||||
// Handle message-level changes (like stop reason and usage information)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
if stopReason := delta.Get("stop_reason"); stopReason.Exists() {
|
||||
switch stopReason.String() {
|
||||
@@ -178,7 +210,7 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
}
|
||||
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
// Basic token counts
|
||||
// Basic token counts for prompt and completion
|
||||
inputTokens := usage.Get("input_tokens").Int()
|
||||
outputTokens := usage.Get("output_tokens").Int()
|
||||
|
||||
@@ -187,7 +219,7 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", inputTokens+outputTokens)
|
||||
|
||||
// Add cache-related token counts if present (Anthropic API cache fields)
|
||||
// Add cache-related token counts if present (Claude Code API cache fields)
|
||||
if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() {
|
||||
template, _ = sjson.Set(template, "usageMetadata.cachedContentTokenCount", cacheCreationTokens.Int())
|
||||
}
|
||||
@@ -210,10 +242,10 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
|
||||
return []string{template}
|
||||
case "message_stop":
|
||||
// Final message with usage information
|
||||
// Final message with usage information - no additional output needed
|
||||
return []string{}
|
||||
case "error":
|
||||
// Handle error responses
|
||||
// Handle error responses and convert to Gemini error format
|
||||
errorMsg := root.Get("error.message").String()
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unknown error occurred"
|
||||
@@ -225,290 +257,11 @@ func ConvertAnthropicResponseToGemini(rawJSON []byte, param *ConvertAnthropicRes
|
||||
return []string{errorResponse}
|
||||
|
||||
default:
|
||||
// Unknown event type, return empty
|
||||
// Unknown event type, return empty response
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertAnthropicResponseToGeminiNonStream converts Anthropic streaming events to a single Gemini non-streaming response.
|
||||
// This function processes multiple Anthropic streaming events and aggregates them into a complete
|
||||
// Gemini-compatible JSON response that includes all content parts (including thinking/reasoning),
|
||||
// function calls, and usage metadata. It simulates the streaming process internally but returns
|
||||
// a single consolidated response.
|
||||
func ConvertAnthropicResponseToGeminiNonStream(streamingEvents [][]byte, model string) string {
|
||||
// Base Gemini response template for non-streaming
|
||||
template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
||||
|
||||
// Set model version
|
||||
template, _ = sjson.Set(template, "modelVersion", model)
|
||||
|
||||
// Initialize parameters for streaming conversion
|
||||
param := &ConvertAnthropicResponseToGeminiParams{
|
||||
Model: model,
|
||||
IsStreaming: false,
|
||||
}
|
||||
|
||||
// Process each streaming event and collect parts
|
||||
var allParts []interface{}
|
||||
var finalUsage map[string]interface{}
|
||||
var responseID string
|
||||
var createdAt int64
|
||||
|
||||
for _, eventData := range streamingEvents {
|
||||
if len(eventData) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
root := gjson.ParseBytes(eventData)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Extract response metadata
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
responseID = message.Get("id").String()
|
||||
param.ResponseID = responseID
|
||||
param.Model = message.Get("model").String()
|
||||
|
||||
// Set creation time to current time if not provided
|
||||
createdAt = time.Now().Unix()
|
||||
param.CreatedAt = createdAt
|
||||
}
|
||||
|
||||
case "content_block_start":
|
||||
// Prepare for content block; record tool_use name by index for later functionCall assembly
|
||||
idx := int(root.Get("index").Int())
|
||||
if cb := root.Get("content_block"); cb.Exists() {
|
||||
if cb.Get("type").String() == "tool_use" {
|
||||
if param.ToolUseNames == nil {
|
||||
param.ToolUseNames = map[int]string{}
|
||||
}
|
||||
if name := cb.Get("name"); name.Exists() {
|
||||
param.ToolUseNames[idx] = name.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
|
||||
case "content_block_delta":
|
||||
// Handle content delta (text, thinking, or tool input)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
partJSON := `{"text":""}`
|
||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
||||
allParts = append(allParts, part)
|
||||
}
|
||||
case "thinking_delta":
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
partJSON := `{"thought":true,"text":""}`
|
||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
||||
allParts = append(allParts, part)
|
||||
}
|
||||
case "input_json_delta":
|
||||
// accumulate args partial_json for this index
|
||||
idx := int(root.Get("index").Int())
|
||||
if param.ToolUseArgs == nil {
|
||||
param.ToolUseArgs = map[int]*strings.Builder{}
|
||||
}
|
||||
if _, ok := param.ToolUseArgs[idx]; !ok || param.ToolUseArgs[idx] == nil {
|
||||
param.ToolUseArgs[idx] = &strings.Builder{}
|
||||
}
|
||||
if pj := delta.Get("partial_json"); pj.Exists() {
|
||||
param.ToolUseArgs[idx].WriteString(pj.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "content_block_stop":
|
||||
// Handle tool use completion
|
||||
idx := int(root.Get("index").Int())
|
||||
// Claude's content_block_stop often doesn't include content_block payload (see docs/response-claude.txt)
|
||||
// So we finalize using accumulated state captured during content_block_start and input_json_delta.
|
||||
name := ""
|
||||
if param.ToolUseNames != nil {
|
||||
name = param.ToolUseNames[idx]
|
||||
}
|
||||
var argsTrim string
|
||||
if param.ToolUseArgs != nil {
|
||||
if b := param.ToolUseArgs[idx]; b != nil {
|
||||
argsTrim = strings.TrimSpace(b.String())
|
||||
}
|
||||
}
|
||||
if name != "" || argsTrim != "" {
|
||||
functionCallJSON := `{"functionCall":{"name":"","args":{}}}`
|
||||
if name != "" {
|
||||
functionCallJSON, _ = sjson.Set(functionCallJSON, "functionCall.name", name)
|
||||
}
|
||||
if argsTrim != "" {
|
||||
functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
|
||||
}
|
||||
// Parse back to interface{} for allParts
|
||||
functionCall := gjson.Parse(functionCallJSON).Value().(map[string]interface{})
|
||||
allParts = append(allParts, functionCall)
|
||||
// cleanup used state for this index
|
||||
if param.ToolUseArgs != nil {
|
||||
delete(param.ToolUseArgs, idx)
|
||||
}
|
||||
if param.ToolUseNames != nil {
|
||||
delete(param.ToolUseNames, idx)
|
||||
}
|
||||
}
|
||||
|
||||
case "message_delta":
|
||||
// Extract final usage information using sjson
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
usageJSON := `{}`
|
||||
|
||||
// Basic token counts
|
||||
inputTokens := usage.Get("input_tokens").Int()
|
||||
outputTokens := usage.Get("output_tokens").Int()
|
||||
|
||||
// Set basic usage metadata according to Gemini API specification
|
||||
usageJSON, _ = sjson.Set(usageJSON, "promptTokenCount", inputTokens)
|
||||
usageJSON, _ = sjson.Set(usageJSON, "candidatesTokenCount", outputTokens)
|
||||
usageJSON, _ = sjson.Set(usageJSON, "totalTokenCount", inputTokens+outputTokens)
|
||||
|
||||
// Add cache-related token counts if present (Anthropic API cache fields)
|
||||
if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() {
|
||||
usageJSON, _ = sjson.Set(usageJSON, "cachedContentTokenCount", cacheCreationTokens.Int())
|
||||
}
|
||||
if cacheReadTokens := usage.Get("cache_read_input_tokens"); cacheReadTokens.Exists() {
|
||||
// Add cache read tokens to cached content count
|
||||
existingCacheTokens := usage.Get("cache_creation_input_tokens").Int()
|
||||
totalCacheTokens := existingCacheTokens + cacheReadTokens.Int()
|
||||
usageJSON, _ = sjson.Set(usageJSON, "cachedContentTokenCount", totalCacheTokens)
|
||||
}
|
||||
|
||||
// Add thinking tokens if present (for models with reasoning capabilities)
|
||||
if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() {
|
||||
usageJSON, _ = sjson.Set(usageJSON, "thoughtsTokenCount", thinkingTokens.Int())
|
||||
}
|
||||
|
||||
// Set traffic type (required by Gemini API)
|
||||
usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
|
||||
|
||||
// Convert to map[string]interface{} using gjson
|
||||
finalUsage = gjson.Parse(usageJSON).Value().(map[string]interface{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set response metadata
|
||||
if responseID != "" {
|
||||
template, _ = sjson.Set(template, "responseId", responseID)
|
||||
}
|
||||
if createdAt > 0 {
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix(createdAt, 0).Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
// Consolidate consecutive text parts and thinking parts
|
||||
consolidatedParts := consolidateParts(allParts)
|
||||
|
||||
// Set the consolidated parts array
|
||||
if len(consolidatedParts) > 0 {
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", convertToJSONString(consolidatedParts))
|
||||
}
|
||||
|
||||
// Set usage metadata
|
||||
if finalUsage != nil {
|
||||
template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage))
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
// consolidateParts merges consecutive text parts and thinking parts to create a cleaner response
|
||||
func consolidateParts(parts []interface{}) []interface{} {
|
||||
if len(parts) == 0 {
|
||||
return parts
|
||||
}
|
||||
|
||||
var consolidated []interface{}
|
||||
var currentTextPart strings.Builder
|
||||
var currentThoughtPart strings.Builder
|
||||
var hasText, hasThought bool
|
||||
|
||||
flushText := func() {
|
||||
if hasText && currentTextPart.Len() > 0 {
|
||||
textPartJSON := `{"text":""}`
|
||||
textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
|
||||
textPart := gjson.Parse(textPartJSON).Value().(map[string]interface{})
|
||||
consolidated = append(consolidated, textPart)
|
||||
currentTextPart.Reset()
|
||||
hasText = false
|
||||
}
|
||||
}
|
||||
|
||||
flushThought := func() {
|
||||
if hasThought && currentThoughtPart.Len() > 0 {
|
||||
thoughtPartJSON := `{"thought":true,"text":""}`
|
||||
thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
|
||||
thoughtPart := gjson.Parse(thoughtPartJSON).Value().(map[string]interface{})
|
||||
consolidated = append(consolidated, thoughtPart)
|
||||
currentThoughtPart.Reset()
|
||||
hasThought = false
|
||||
}
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
partMap, ok := part.(map[string]interface{})
|
||||
if !ok {
|
||||
// Flush any pending parts and add this non-text part
|
||||
flushText()
|
||||
flushThought()
|
||||
consolidated = append(consolidated, part)
|
||||
continue
|
||||
}
|
||||
|
||||
if thought, isThought := partMap["thought"]; isThought && thought == true {
|
||||
// This is a thinking part
|
||||
flushText() // Flush any pending text first
|
||||
|
||||
if text, hasTextContent := partMap["text"].(string); hasTextContent {
|
||||
currentThoughtPart.WriteString(text)
|
||||
hasThought = true
|
||||
}
|
||||
} else if text, hasTextContent := partMap["text"].(string); hasTextContent {
|
||||
// This is a regular text part
|
||||
flushThought() // Flush any pending thought first
|
||||
|
||||
currentTextPart.WriteString(text)
|
||||
hasText = true
|
||||
} else {
|
||||
// This is some other type of part (like function call)
|
||||
flushText()
|
||||
flushThought()
|
||||
consolidated = append(consolidated, part)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any remaining parts
|
||||
flushThought() // Flush thought first to maintain order
|
||||
flushText()
|
||||
|
||||
return consolidated
|
||||
}
|
||||
|
||||
// convertToJSONString converts interface{} to JSON string using sjson/gjson
|
||||
func convertToJSONString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case []interface{}:
|
||||
return convertArrayToJSON(val)
|
||||
case map[string]interface{}:
|
||||
return convertMapToJSON(val)
|
||||
default:
|
||||
// For simple types, create a temporary JSON and extract the value
|
||||
temp := `{"temp":null}`
|
||||
temp, _ = sjson.Set(temp, "temp", val)
|
||||
return gjson.Get(temp, "temp").Raw
|
||||
}
|
||||
}
|
||||
|
||||
// convertArrayToJSON converts []interface{} to JSON array string
|
||||
func convertArrayToJSON(arr []interface{}) string {
|
||||
result := "[]"
|
||||
@@ -553,3 +306,320 @@ func convertMapToJSON(m map[string]interface{}) string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response.
|
||||
// This function processes the complete Claude Code response and transforms it into a single Gemini-compatible
|
||||
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
||||
// the information into a single response that matches the Gemini API format.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response
|
||||
// - rawJSON: The raw JSON response from the Claude Code API
|
||||
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||
//
|
||||
// Returns:
|
||||
// - string: A Gemini-compatible JSON response containing all message content and metadata
|
||||
func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, rawJSON []byte, _ *any) string {
|
||||
// Base Gemini response template for non-streaming with default values
|
||||
template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
||||
|
||||
// Set model version
|
||||
template, _ = sjson.Set(template, "modelVersion", modelName)
|
||||
|
||||
streamingEvents := make([][]byte, 0)
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
||||
buffer := make([]byte, 10240*1024)
|
||||
scanner.Buffer(buffer, 10240*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// log.Debug(string(line))
|
||||
if bytes.HasPrefix(line, dataTag) {
|
||||
jsonData := line[6:]
|
||||
streamingEvents = append(streamingEvents, jsonData)
|
||||
}
|
||||
}
|
||||
// log.Debug("streamingEvents: ", streamingEvents)
|
||||
// log.Debug("rawJSON: ", string(rawJSON))
|
||||
|
||||
// Initialize parameters for streaming conversion with proper state management
|
||||
newParam := &ConvertAnthropicResponseToGeminiParams{
|
||||
Model: modelName,
|
||||
CreatedAt: 0,
|
||||
ResponseID: "",
|
||||
LastStorageOutput: "",
|
||||
IsStreaming: false,
|
||||
ToolUseNames: nil,
|
||||
ToolUseArgs: nil,
|
||||
}
|
||||
|
||||
// Process each streaming event and collect parts
|
||||
var allParts []interface{}
|
||||
var finalUsage map[string]interface{}
|
||||
var responseID string
|
||||
var createdAt int64
|
||||
|
||||
for _, eventData := range streamingEvents {
|
||||
if len(eventData) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
root := gjson.ParseBytes(eventData)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Extract response metadata including ID, model, and creation time
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
responseID = message.Get("id").String()
|
||||
newParam.ResponseID = responseID
|
||||
newParam.Model = message.Get("model").String()
|
||||
|
||||
// Set creation time to current time if not provided
|
||||
createdAt = time.Now().Unix()
|
||||
newParam.CreatedAt = createdAt
|
||||
}
|
||||
|
||||
case "content_block_start":
|
||||
// Prepare for content block; record tool_use name by index for later functionCall assembly
|
||||
idx := int(root.Get("index").Int())
|
||||
if cb := root.Get("content_block"); cb.Exists() {
|
||||
if cb.Get("type").String() == "tool_use" {
|
||||
if newParam.ToolUseNames == nil {
|
||||
newParam.ToolUseNames = map[int]string{}
|
||||
}
|
||||
if name := cb.Get("name"); name.Exists() {
|
||||
newParam.ToolUseNames[idx] = name.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
continue
|
||||
|
||||
case "content_block_delta":
|
||||
// Handle content delta (text, thinking, or tool input)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
// Process regular text content
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
partJSON := `{"text":""}`
|
||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
||||
allParts = append(allParts, part)
|
||||
}
|
||||
case "thinking_delta":
|
||||
// Process reasoning/thinking content
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
partJSON := `{"thought":true,"text":""}`
|
||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
||||
part := gjson.Parse(partJSON).Value().(map[string]interface{})
|
||||
allParts = append(allParts, part)
|
||||
}
|
||||
case "input_json_delta":
|
||||
// accumulate args partial_json for this index
|
||||
idx := int(root.Get("index").Int())
|
||||
if newParam.ToolUseArgs == nil {
|
||||
newParam.ToolUseArgs = map[int]*strings.Builder{}
|
||||
}
|
||||
if _, ok := newParam.ToolUseArgs[idx]; !ok || newParam.ToolUseArgs[idx] == nil {
|
||||
newParam.ToolUseArgs[idx] = &strings.Builder{}
|
||||
}
|
||||
if pj := delta.Get("partial_json"); pj.Exists() {
|
||||
newParam.ToolUseArgs[idx].WriteString(pj.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "content_block_stop":
|
||||
// Handle tool use completion by assembling accumulated arguments
|
||||
idx := int(root.Get("index").Int())
|
||||
// Claude's content_block_stop often doesn't include content_block payload (see docs/response-claude.txt)
|
||||
// So we finalize using accumulated state captured during content_block_start and input_json_delta.
|
||||
name := ""
|
||||
if newParam.ToolUseNames != nil {
|
||||
name = newParam.ToolUseNames[idx]
|
||||
}
|
||||
var argsTrim string
|
||||
if newParam.ToolUseArgs != nil {
|
||||
if b := newParam.ToolUseArgs[idx]; b != nil {
|
||||
argsTrim = strings.TrimSpace(b.String())
|
||||
}
|
||||
}
|
||||
if name != "" || argsTrim != "" {
|
||||
functionCallJSON := `{"functionCall":{"name":"","args":{}}}`
|
||||
if name != "" {
|
||||
functionCallJSON, _ = sjson.Set(functionCallJSON, "functionCall.name", name)
|
||||
}
|
||||
if argsTrim != "" {
|
||||
functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
|
||||
}
|
||||
// Parse back to interface{} for allParts
|
||||
functionCall := gjson.Parse(functionCallJSON).Value().(map[string]interface{})
|
||||
allParts = append(allParts, functionCall)
|
||||
// cleanup used state for this index
|
||||
if newParam.ToolUseArgs != nil {
|
||||
delete(newParam.ToolUseArgs, idx)
|
||||
}
|
||||
if newParam.ToolUseNames != nil {
|
||||
delete(newParam.ToolUseNames, idx)
|
||||
}
|
||||
}
|
||||
|
||||
case "message_delta":
|
||||
// Extract final usage information using sjson for token counts and metadata
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
usageJSON := `{}`
|
||||
|
||||
// Basic token counts for prompt and completion
|
||||
inputTokens := usage.Get("input_tokens").Int()
|
||||
outputTokens := usage.Get("output_tokens").Int()
|
||||
|
||||
// Set basic usage metadata according to Gemini API specification
|
||||
usageJSON, _ = sjson.Set(usageJSON, "promptTokenCount", inputTokens)
|
||||
usageJSON, _ = sjson.Set(usageJSON, "candidatesTokenCount", outputTokens)
|
||||
usageJSON, _ = sjson.Set(usageJSON, "totalTokenCount", inputTokens+outputTokens)
|
||||
|
||||
// Add cache-related token counts if present (Claude Code API cache fields)
|
||||
if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() {
|
||||
usageJSON, _ = sjson.Set(usageJSON, "cachedContentTokenCount", cacheCreationTokens.Int())
|
||||
}
|
||||
if cacheReadTokens := usage.Get("cache_read_input_tokens"); cacheReadTokens.Exists() {
|
||||
// Add cache read tokens to cached content count
|
||||
existingCacheTokens := usage.Get("cache_creation_input_tokens").Int()
|
||||
totalCacheTokens := existingCacheTokens + cacheReadTokens.Int()
|
||||
usageJSON, _ = sjson.Set(usageJSON, "cachedContentTokenCount", totalCacheTokens)
|
||||
}
|
||||
|
||||
// Add thinking tokens if present (for models with reasoning capabilities)
|
||||
if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() {
|
||||
usageJSON, _ = sjson.Set(usageJSON, "thoughtsTokenCount", thinkingTokens.Int())
|
||||
}
|
||||
|
||||
// Set traffic type (required by Gemini API)
|
||||
usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
|
||||
|
||||
// Convert to map[string]interface{} using gjson
|
||||
finalUsage = gjson.Parse(usageJSON).Value().(map[string]interface{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set response metadata
|
||||
if responseID != "" {
|
||||
template, _ = sjson.Set(template, "responseId", responseID)
|
||||
}
|
||||
if createdAt > 0 {
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix(createdAt, 0).Format(time.RFC3339Nano))
|
||||
}
|
||||
|
||||
// Consolidate consecutive text parts and thinking parts for cleaner output
|
||||
consolidatedParts := consolidateParts(allParts)
|
||||
|
||||
// Set the consolidated parts array
|
||||
if len(consolidatedParts) > 0 {
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts", convertToJSONString(consolidatedParts))
|
||||
}
|
||||
|
||||
// Set usage metadata
|
||||
if finalUsage != nil {
|
||||
template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage))
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
// consolidateParts merges consecutive text parts and thinking parts to create a cleaner response.
|
||||
// This function processes the parts array to combine adjacent text elements and thinking elements
|
||||
// into single consolidated parts, which results in a more readable and efficient response structure.
|
||||
// Tool calls and other non-text parts are preserved as separate elements.
|
||||
func consolidateParts(parts []interface{}) []interface{} {
|
||||
if len(parts) == 0 {
|
||||
return parts
|
||||
}
|
||||
|
||||
var consolidated []interface{}
|
||||
var currentTextPart strings.Builder
|
||||
var currentThoughtPart strings.Builder
|
||||
var hasText, hasThought bool
|
||||
|
||||
flushText := func() {
|
||||
// Flush accumulated text content to the consolidated parts array
|
||||
if hasText && currentTextPart.Len() > 0 {
|
||||
textPartJSON := `{"text":""}`
|
||||
textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
|
||||
textPart := gjson.Parse(textPartJSON).Value().(map[string]interface{})
|
||||
consolidated = append(consolidated, textPart)
|
||||
currentTextPart.Reset()
|
||||
hasText = false
|
||||
}
|
||||
}
|
||||
|
||||
flushThought := func() {
|
||||
// Flush accumulated thinking content to the consolidated parts array
|
||||
if hasThought && currentThoughtPart.Len() > 0 {
|
||||
thoughtPartJSON := `{"thought":true,"text":""}`
|
||||
thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
|
||||
thoughtPart := gjson.Parse(thoughtPartJSON).Value().(map[string]interface{})
|
||||
consolidated = append(consolidated, thoughtPart)
|
||||
currentThoughtPart.Reset()
|
||||
hasThought = false
|
||||
}
|
||||
}
|
||||
|
||||
for _, part := range parts {
|
||||
partMap, ok := part.(map[string]interface{})
|
||||
if !ok {
|
||||
// Flush any pending parts and add this non-text part
|
||||
flushText()
|
||||
flushThought()
|
||||
consolidated = append(consolidated, part)
|
||||
continue
|
||||
}
|
||||
|
||||
if thought, isThought := partMap["thought"]; isThought && thought == true {
|
||||
// This is a thinking part - flush any pending text first
|
||||
flushText() // Flush any pending text first
|
||||
|
||||
if text, hasTextContent := partMap["text"].(string); hasTextContent {
|
||||
currentThoughtPart.WriteString(text)
|
||||
hasThought = true
|
||||
}
|
||||
} else if text, hasTextContent := partMap["text"].(string); hasTextContent {
|
||||
// This is a regular text part - flush any pending thought first
|
||||
flushThought() // Flush any pending thought first
|
||||
|
||||
currentTextPart.WriteString(text)
|
||||
hasText = true
|
||||
} else {
|
||||
// This is some other type of part (like function call) - flush both text and thought
|
||||
flushText()
|
||||
flushThought()
|
||||
consolidated = append(consolidated, part)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush any remaining parts
|
||||
flushThought() // Flush thought first to maintain order
|
||||
flushText()
|
||||
|
||||
return consolidated
|
||||
}
|
||||
|
||||
// convertToJSONString converts interface{} to JSON string using sjson/gjson.
|
||||
// This function provides a consistent way to serialize different data types to JSON strings
|
||||
// for inclusion in the Gemini API response structure.
|
||||
func convertToJSONString(v interface{}) string {
|
||||
switch val := v.(type) {
|
||||
case []interface{}:
|
||||
return convertArrayToJSON(val)
|
||||
case map[string]interface{}:
|
||||
return convertMapToJSON(val)
|
||||
default:
|
||||
// For simple types, create a temporary JSON and extract the value
|
||||
temp := `{"temp":null}`
|
||||
temp, _ = sjson.Set(temp, "temp", val)
|
||||
return gjson.Get(temp, "temp").Raw
|
||||
}
|
||||
}
|
||||
|
||||
19
internal/translator/claude/gemini/init.go
Normal file
19
internal/translator/claude/gemini/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package gemini
|
||||
|
||||
import (
|
||||
. "github.com/luispater/CLIProxyAPI/internal/constant"
|
||||
"github.com/luispater/CLIProxyAPI/internal/interfaces"
|
||||
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
GEMINI,
|
||||
CLAUDE,
|
||||
ConvertGeminiRequestToClaude,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertClaudeResponseToGemini,
|
||||
NonStream: ConvertClaudeResponseToGeminiNonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Package openai provides request translation functionality for OpenAI to Anthropic API.
|
||||
// It handles parsing and transforming OpenAI Chat Completions API requests into Anthropic API format,
|
||||
// Package openai provides request translation functionality for OpenAI to Claude Code API compatibility.
|
||||
// It handles parsing and transforming OpenAI Chat Completions 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 OpenAI API format and Anthropic API's expected format.
|
||||
// between OpenAI API format and Claude Code API's expected format.
|
||||
package openai
|
||||
|
||||
import (
|
||||
@@ -15,20 +15,35 @@ import (
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertOpenAIRequestToAnthropic parses and transforms an OpenAI Chat Completions API request into Anthropic API format.
|
||||
// ConvertOpenAIRequestToClaude parses and transforms an OpenAI Chat Completions 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 Anthropic API.
|
||||
func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
// Base Anthropic API template
|
||||
// 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 parameter extraction (max_tokens, temperature, top_p, etc.)
|
||||
// 2. Message content conversion from OpenAI to Claude Code format
|
||||
// 3. Tool call and tool result handling with proper ID mapping
|
||||
// 4. Image data conversion from OpenAI data URLs to Claude Code base64 format
|
||||
// 5. Stop sequence and streaming configuration handling
|
||||
//
|
||||
// Parameters:
|
||||
// - modelName: The name of the model to use for the request
|
||||
// - rawJSON: The raw JSON request data from the OpenAI 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 ConvertOpenAIRequestToClaude(modelName string, rawJSON []byte, stream bool) []byte {
|
||||
// Base Claude Code API template with default max_tokens value
|
||||
out := `{"model":"","max_tokens":32000,"messages":[]}`
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Helper for generating tool call IDs in the form: toolu_<alphanum>
|
||||
// This ensures unique identifiers for tool calls in the Claude Code format
|
||||
genToolCallID := func() string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var b strings.Builder
|
||||
// 24 chars random suffix
|
||||
// 24 chars random suffix for uniqueness
|
||||
for i := 0; i < 24; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
b.WriteByte(letters[n.Int64()])
|
||||
@@ -36,28 +51,25 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
return "toolu_" + b.String()
|
||||
}
|
||||
|
||||
// Model mapping
|
||||
if model := root.Get("model"); model.Exists() {
|
||||
modelStr := model.String()
|
||||
out, _ = sjson.Set(out, "model", modelStr)
|
||||
}
|
||||
// Model mapping to specify which Claude Code model to use
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
|
||||
// Max tokens
|
||||
// Max tokens configuration with fallback to default value
|
||||
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
|
||||
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
|
||||
// Temperature
|
||||
// Temperature setting for controlling response randomness
|
||||
if temp := root.Get("temperature"); temp.Exists() {
|
||||
out, _ = sjson.Set(out, "temperature", temp.Float())
|
||||
}
|
||||
|
||||
// Top P
|
||||
// Top P setting for nucleus sampling
|
||||
if topP := root.Get("top_p"); topP.Exists() {
|
||||
out, _ = sjson.Set(out, "top_p", topP.Float())
|
||||
}
|
||||
|
||||
// Stop sequences
|
||||
// Stop sequences configuration for custom termination conditions
|
||||
if stop := root.Get("stop"); stop.Exists() {
|
||||
if stop.IsArray() {
|
||||
var stopSequences []string
|
||||
@@ -73,12 +85,10 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Stream
|
||||
if stream := root.Get("stream"); stream.Exists() {
|
||||
out, _ = sjson.Set(out, "stream", stream.Bool())
|
||||
}
|
||||
// Stream configuration to enable or disable streaming responses
|
||||
out, _ = sjson.Set(out, "stream", stream)
|
||||
|
||||
// Process messages
|
||||
// Process messages and transform them to Claude Code format
|
||||
var anthropicMessages []interface{}
|
||||
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
||||
|
||||
@@ -89,7 +99,7 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
|
||||
switch role {
|
||||
case "system", "user", "assistant":
|
||||
// Create Anthropic message
|
||||
// Create Claude Code message with appropriate role mapping
|
||||
if role == "system" {
|
||||
role = "user"
|
||||
}
|
||||
@@ -99,9 +109,9 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
"content": []interface{}{},
|
||||
}
|
||||
|
||||
// Handle content
|
||||
// Handle content based on its type (string or array)
|
||||
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
||||
// Simple text content
|
||||
// Simple text content conversion
|
||||
msg["content"] = []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "text",
|
||||
@@ -109,23 +119,24 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
},
|
||||
}
|
||||
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||
// Array of content parts
|
||||
// Array of content parts processing
|
||||
var contentParts []interface{}
|
||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||
partType := part.Get("type").String()
|
||||
|
||||
switch partType {
|
||||
case "text":
|
||||
// Text part conversion
|
||||
contentParts = append(contentParts, map[string]interface{}{
|
||||
"type": "text",
|
||||
"text": part.Get("text").String(),
|
||||
})
|
||||
|
||||
case "image_url":
|
||||
// Convert OpenAI image format to Anthropic format
|
||||
// Convert OpenAI image format to Claude Code format
|
||||
imageURL := part.Get("image_url.url").String()
|
||||
if strings.HasPrefix(imageURL, "data:") {
|
||||
// Extract base64 data and media type
|
||||
// Extract base64 data and media type from data URL
|
||||
parts := strings.Split(imageURL, ",")
|
||||
if len(parts) == 2 {
|
||||
mediaTypePart := strings.Split(parts[0], ";")[0]
|
||||
@@ -177,7 +188,7 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
"name": function.Get("name").String(),
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
// Parse arguments for the tool call
|
||||
if args := function.Get("arguments"); args.Exists() {
|
||||
argsStr := args.String()
|
||||
if argsStr != "" {
|
||||
@@ -204,11 +215,11 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
anthropicMessages = append(anthropicMessages, msg)
|
||||
|
||||
case "tool":
|
||||
// Handle tool result messages
|
||||
// Handle tool result messages conversion
|
||||
toolCallID := message.Get("tool_call_id").String()
|
||||
content := message.Get("content").String()
|
||||
|
||||
// Create tool result message
|
||||
// Create tool result message in Claude Code format
|
||||
msg := map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": []interface{}{
|
||||
@@ -226,13 +237,13 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
})
|
||||
}
|
||||
|
||||
// Set messages
|
||||
// Set messages in the output template
|
||||
if len(anthropicMessages) > 0 {
|
||||
messagesJSON, _ := json.Marshal(anthropicMessages)
|
||||
out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
|
||||
}
|
||||
|
||||
// Tools mapping: OpenAI tools -> Anthropic tools
|
||||
// Tools mapping: OpenAI tools -> Claude Code tools
|
||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||
var anthropicTools []interface{}
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
@@ -243,9 +254,11 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
"description": function.Get("description").String(),
|
||||
}
|
||||
|
||||
// Convert parameters schema
|
||||
// Convert parameters schema for the tool
|
||||
if parameters := function.Get("parameters"); parameters.Exists() {
|
||||
anthropicTool["input_schema"] = parameters.Value()
|
||||
} else if parameters = function.Get("parametersJsonSchema"); parameters.Exists() {
|
||||
anthropicTool["input_schema"] = parameters.Value()
|
||||
}
|
||||
|
||||
anthropicTools = append(anthropicTools, anthropicTool)
|
||||
@@ -259,21 +272,21 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Tool choice mapping
|
||||
// Tool choice mapping from OpenAI format to Claude Code format
|
||||
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
||||
switch toolChoice.Type {
|
||||
case gjson.String:
|
||||
choice := toolChoice.String()
|
||||
switch choice {
|
||||
case "none":
|
||||
// Don't set tool_choice, Anthropic will not use tools
|
||||
// Don't set tool_choice, Claude Code will not use tools
|
||||
case "auto":
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
|
||||
case "required":
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
|
||||
}
|
||||
case gjson.JSON:
|
||||
// Specific tool choice
|
||||
// Specific tool choice mapping
|
||||
if toolChoice.Get("type").String() == "function" {
|
||||
functionName := toolChoice.Get("function.name").String()
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{
|
||||
@@ -285,5 +298,5 @@ func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
return []byte(out)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// Package openai provides response translation functionality for Anthropic to OpenAI API.
|
||||
// This package handles the conversion of Anthropic API responses into OpenAI Chat Completions-compatible
|
||||
// Package openai provides response translation functionality for Claude Code to OpenAI API compatibility.
|
||||
// This package handles the conversion of Claude Code API responses into OpenAI Chat Completions-compatible
|
||||
// JSON format, transforming streaming events and non-streaming responses into the format
|
||||
// expected by OpenAI API clients. It supports both streaming and non-streaming modes,
|
||||
// handling text content, tool calls, and usage metadata appropriately.
|
||||
// handling text content, tool calls, reasoning content, and usage metadata appropriately.
|
||||
package openai
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,6 +17,10 @@ import (
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
var (
|
||||
dataTag = []byte("data: ")
|
||||
)
|
||||
|
||||
// ConvertAnthropicResponseToOpenAIParams holds parameters for response conversion
|
||||
type ConvertAnthropicResponseToOpenAIParams struct {
|
||||
CreatedAt int64
|
||||
@@ -30,10 +37,33 @@ type ToolCallAccumulator struct {
|
||||
Arguments strings.Builder
|
||||
}
|
||||
|
||||
// ConvertAnthropicResponseToOpenAI converts Anthropic streaming response format to OpenAI Chat Completions format.
|
||||
// This function processes various Anthropic event types and transforms them into OpenAI-compatible JSON responses.
|
||||
// It handles text content, tool calls, and usage metadata, outputting responses that match the OpenAI API format.
|
||||
func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicResponseToOpenAIParams) []string {
|
||||
// ConvertClaudeResponseToOpenAI converts Claude Code streaming response format to OpenAI Chat Completions format.
|
||||
// This function processes various Claude Code event types and transforms them into OpenAI-compatible JSON responses.
|
||||
// It handles text content, tool calls, reasoning content, and usage metadata, outputting responses that match
|
||||
// the OpenAI API format. The function supports incremental updates for streaming responses.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response
|
||||
// - rawJSON: The raw JSON response from the Claude Code API
|
||||
// - param: A pointer to a parameter object for maintaining state between calls
|
||||
//
|
||||
// Returns:
|
||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
||||
func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, rawJSON []byte, param *any) []string {
|
||||
if *param == nil {
|
||||
*param = &ConvertAnthropicResponseToOpenAIParams{
|
||||
CreatedAt: 0,
|
||||
ResponseID: "",
|
||||
FinishReason: "",
|
||||
}
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||
return []string{}
|
||||
}
|
||||
rawJSON = rawJSON[6:]
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
@@ -41,57 +71,55 @@ func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicRes
|
||||
template := `{"id":"","object":"chat.completion.chunk","created":0,"model":"","choices":[{"index":0,"delta":{},"finish_reason":null}]}`
|
||||
|
||||
// Set model
|
||||
modelResult := gjson.GetBytes(rawJSON, "model")
|
||||
modelName := modelResult.String()
|
||||
if modelName != "" {
|
||||
template, _ = sjson.Set(template, "model", modelName)
|
||||
}
|
||||
|
||||
// Set response ID and creation time
|
||||
if param.ResponseID != "" {
|
||||
template, _ = sjson.Set(template, "id", param.ResponseID)
|
||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID != "" {
|
||||
template, _ = sjson.Set(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID)
|
||||
}
|
||||
if param.CreatedAt > 0 {
|
||||
template, _ = sjson.Set(template, "created", param.CreatedAt)
|
||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt > 0 {
|
||||
template, _ = sjson.Set(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt)
|
||||
}
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Initialize response with message metadata
|
||||
// Initialize response with message metadata when a new message begins
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
param.ResponseID = message.Get("id").String()
|
||||
param.CreatedAt = time.Now().Unix()
|
||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID = message.Get("id").String()
|
||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt = time.Now().Unix()
|
||||
|
||||
template, _ = sjson.Set(template, "id", param.ResponseID)
|
||||
template, _ = sjson.Set(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID)
|
||||
template, _ = sjson.Set(template, "model", modelName)
|
||||
template, _ = sjson.Set(template, "created", param.CreatedAt)
|
||||
template, _ = sjson.Set(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt)
|
||||
|
||||
// Set initial role
|
||||
// Set initial role to assistant for the response
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
|
||||
// Initialize tool calls accumulator
|
||||
if param.ToolCallsAccumulator == nil {
|
||||
param.ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
||||
// Initialize tool calls accumulator for tracking tool call progress
|
||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator == nil {
|
||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "content_block_start":
|
||||
// Start of a content block
|
||||
// Start of a content block (text, tool use, or reasoning)
|
||||
if contentBlock := root.Get("content_block"); contentBlock.Exists() {
|
||||
blockType := contentBlock.Get("type").String()
|
||||
|
||||
if blockType == "tool_use" {
|
||||
// Start of tool call - initialize accumulator
|
||||
// Start of tool call - initialize accumulator to track arguments
|
||||
toolCallID := contentBlock.Get("id").String()
|
||||
toolName := contentBlock.Get("name").String()
|
||||
index := int(root.Get("index").Int())
|
||||
|
||||
if param.ToolCallsAccumulator == nil {
|
||||
param.ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator == nil {
|
||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
||||
}
|
||||
|
||||
param.ToolCallsAccumulator[index] = &ToolCallAccumulator{
|
||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator[index] = &ToolCallAccumulator{
|
||||
ID: toolCallID,
|
||||
Name: toolName,
|
||||
}
|
||||
@@ -103,23 +131,23 @@ func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicRes
|
||||
return []string{template}
|
||||
|
||||
case "content_block_delta":
|
||||
// Handle content delta (text or tool use)
|
||||
// Handle content delta (text, tool use arguments, or reasoning content)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
// Text content delta
|
||||
// Text content delta - send incremental text updates
|
||||
if text := delta.Get("text"); text.Exists() {
|
||||
template, _ = sjson.Set(template, "choices.0.delta.content", text.String())
|
||||
}
|
||||
|
||||
case "input_json_delta":
|
||||
// Tool use input delta - accumulate arguments
|
||||
// Tool use input delta - accumulate arguments for tool calls
|
||||
if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
|
||||
index := int(root.Get("index").Int())
|
||||
if param.ToolCallsAccumulator != nil {
|
||||
if accumulator, exists := param.ToolCallsAccumulator[index]; exists {
|
||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator != nil {
|
||||
if accumulator, exists := (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator[index]; exists {
|
||||
accumulator.Arguments.WriteString(partialJSON.String())
|
||||
}
|
||||
}
|
||||
@@ -133,9 +161,9 @@ func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicRes
|
||||
case "content_block_stop":
|
||||
// End of content block - output complete tool call if it's a tool_use block
|
||||
index := int(root.Get("index").Int())
|
||||
if param.ToolCallsAccumulator != nil {
|
||||
if accumulator, exists := param.ToolCallsAccumulator[index]; exists {
|
||||
// Build complete tool call
|
||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator != nil {
|
||||
if accumulator, exists := (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator[index]; exists {
|
||||
// Build complete tool call with accumulated arguments
|
||||
arguments := accumulator.Arguments.String()
|
||||
if arguments == "" {
|
||||
arguments = "{}"
|
||||
@@ -154,7 +182,7 @@ func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicRes
|
||||
template, _ = sjson.Set(template, "choices.0.delta.tool_calls", []interface{}{toolCall})
|
||||
|
||||
// Clean up the accumulator for this index
|
||||
delete(param.ToolCallsAccumulator, index)
|
||||
delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
|
||||
|
||||
return []string{template}
|
||||
}
|
||||
@@ -162,15 +190,15 @@ func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicRes
|
||||
return []string{}
|
||||
|
||||
case "message_delta":
|
||||
// Handle message-level changes
|
||||
// Handle message-level changes including stop reason and usage
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
if stopReason := delta.Get("stop_reason"); stopReason.Exists() {
|
||||
param.FinishReason = mapAnthropicStopReasonToOpenAI(stopReason.String())
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", param.FinishReason)
|
||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).FinishReason = mapAnthropicStopReasonToOpenAI(stopReason.String())
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", (*param).(*ConvertAnthropicResponseToOpenAIParams).FinishReason)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information
|
||||
// Handle usage information for token counts
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
usageObj := map[string]interface{}{
|
||||
"prompt_tokens": usage.Get("input_tokens").Int(),
|
||||
@@ -182,15 +210,15 @@ func ConvertAnthropicResponseToOpenAI(rawJSON []byte, param *ConvertAnthropicRes
|
||||
return []string{template}
|
||||
|
||||
case "message_stop":
|
||||
// Final message - send [DONE]
|
||||
return []string{"[DONE]\n"}
|
||||
// Final message event - no additional output needed
|
||||
return []string{}
|
||||
|
||||
case "ping":
|
||||
// Ping events - ignore
|
||||
// Ping events for keeping connection alive - no output needed
|
||||
return []string{}
|
||||
|
||||
case "error":
|
||||
// Error event
|
||||
// Error event - format and return error response
|
||||
if errorData := root.Get("error"); errorData.Exists() {
|
||||
errorResponse := map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
@@ -225,9 +253,34 @@ func mapAnthropicStopReasonToOpenAI(anthropicReason string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// ConvertAnthropicStreamingResponseToOpenAINonStream aggregates streaming chunks into a single non-streaming response
|
||||
// following OpenAI Chat Completions API format with reasoning content support
|
||||
func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string {
|
||||
// ConvertClaudeResponseToOpenAINonStream converts a non-streaming Claude Code response to a non-streaming OpenAI response.
|
||||
// This function processes the complete Claude Code response and transforms it into a single OpenAI-compatible
|
||||
// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all
|
||||
// the information into a single response that matches the OpenAI API format.
|
||||
//
|
||||
// Parameters:
|
||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
||||
// - modelName: The name of the model being used for the response (unused in current implementation)
|
||||
// - rawJSON: The raw JSON response from the Claude Code API
|
||||
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||
//
|
||||
// Returns:
|
||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
||||
func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, rawJSON []byte, _ *any) string {
|
||||
chunks := make([][]byte, 0)
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(rawJSON))
|
||||
buffer := make([]byte, 10240*1024)
|
||||
scanner.Buffer(buffer, 10240*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// log.Debug(string(line))
|
||||
if !bytes.HasPrefix(line, dataTag) {
|
||||
continue
|
||||
}
|
||||
chunks = append(chunks, line[6:])
|
||||
}
|
||||
|
||||
// Base OpenAI non-streaming response template
|
||||
out := `{"id":"","object":"chat.completion","created":0,"model":"","choices":[{"index":0,"message":{"role":"assistant","content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`
|
||||
|
||||
@@ -250,6 +303,7 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Extract initial message metadata including ID, model, and input token count
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
messageID = message.Get("id").String()
|
||||
model = message.Get("model").String()
|
||||
@@ -260,14 +314,14 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
}
|
||||
|
||||
case "content_block_start":
|
||||
// Handle different content block types
|
||||
// Handle different content block types at the beginning
|
||||
if contentBlock := root.Get("content_block"); contentBlock.Exists() {
|
||||
blockType := contentBlock.Get("type").String()
|
||||
if blockType == "thinking" {
|
||||
// Start of thinking/reasoning content
|
||||
// Start of thinking/reasoning content - skip for now as it's handled in delta
|
||||
continue
|
||||
} else if blockType == "tool_use" {
|
||||
// Initialize tool call tracking
|
||||
// Initialize tool call tracking for this index
|
||||
index := int(root.Get("index").Int())
|
||||
toolCallsMap[index] = map[string]interface{}{
|
||||
"id": contentBlock.Get("id").String(),
|
||||
@@ -283,15 +337,17 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
}
|
||||
|
||||
case "content_block_delta":
|
||||
// Process incremental content updates
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
// Accumulate text content
|
||||
if text := delta.Get("text"); text.Exists() {
|
||||
contentParts = append(contentParts, text.String())
|
||||
}
|
||||
case "thinking_delta":
|
||||
// Anthropic thinking content -> OpenAI reasoning content
|
||||
// Accumulate reasoning/thinking content
|
||||
if thinking := delta.Get("thinking"); thinking.Exists() {
|
||||
reasoningParts = append(reasoningParts, thinking.String())
|
||||
}
|
||||
@@ -308,11 +364,11 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
}
|
||||
|
||||
case "content_block_stop":
|
||||
// Finalize tool call arguments for this index
|
||||
// Finalize tool call arguments for this index when content block ends
|
||||
index := int(root.Get("index").Int())
|
||||
if toolCall, exists := toolCallsMap[index]; exists {
|
||||
if builder, argsExists := toolCallArgsMap[index]; argsExists {
|
||||
// Set the accumulated arguments
|
||||
// Set the accumulated arguments for the tool call
|
||||
arguments := builder.String()
|
||||
if arguments == "" {
|
||||
arguments = "{}"
|
||||
@@ -322,6 +378,7 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
}
|
||||
|
||||
case "message_delta":
|
||||
// Extract stop reason and output token count when message ends
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
if sr := delta.Get("stop_reason"); sr.Exists() {
|
||||
stopReason = sr.String()
|
||||
@@ -329,7 +386,7 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
}
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
outputTokens = usage.Get("output_tokens").Int()
|
||||
// Estimate reasoning tokens from thinking content
|
||||
// Estimate reasoning tokens from accumulated thinking content
|
||||
if len(reasoningParts) > 0 {
|
||||
reasoningTokens = int64(len(strings.Join(reasoningParts, "")) / 4) // Rough estimation
|
||||
}
|
||||
@@ -337,12 +394,12 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
}
|
||||
}
|
||||
|
||||
// Set basic response fields
|
||||
// Set basic response fields including message ID, creation time, and model
|
||||
out, _ = sjson.Set(out, "id", messageID)
|
||||
out, _ = sjson.Set(out, "created", createdAt)
|
||||
out, _ = sjson.Set(out, "model", model)
|
||||
|
||||
// Set message content
|
||||
// Set message content by combining all text parts
|
||||
messageContent := strings.Join(contentParts, "")
|
||||
out, _ = sjson.Set(out, "choices.0.message.content", messageContent)
|
||||
|
||||
@@ -353,7 +410,7 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
out, _ = sjson.Set(out, "choices.0.message.reasoning", reasoningContent)
|
||||
}
|
||||
|
||||
// Set tool calls if any
|
||||
// Set tool calls if any were accumulated during processing
|
||||
if len(toolCallsMap) > 0 {
|
||||
// Convert tool calls map to array, preserving order by index
|
||||
var toolCallsArray []interface{}
|
||||
@@ -380,13 +437,13 @@ func ConvertAnthropicStreamingResponseToOpenAINonStream(chunks [][]byte) string
|
||||
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
||||
}
|
||||
|
||||
// Set usage information
|
||||
// Set usage information including prompt tokens, completion tokens, and total tokens
|
||||
totalTokens := inputTokens + outputTokens
|
||||
out, _ = sjson.Set(out, "usage.prompt_tokens", inputTokens)
|
||||
out, _ = sjson.Set(out, "usage.completion_tokens", outputTokens)
|
||||
out, _ = sjson.Set(out, "usage.total_tokens", totalTokens)
|
||||
|
||||
// Add reasoning tokens to usage details if available
|
||||
// Add reasoning tokens to usage details if any reasoning content was processed
|
||||
if reasoningTokens > 0 {
|
||||
out, _ = sjson.Set(out, "usage.completion_tokens_details.reasoning_tokens", reasoningTokens)
|
||||
}
|
||||
|
||||
19
internal/translator/claude/openai/init.go
Normal file
19
internal/translator/claude/openai/init.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package openai
|
||||
|
||||
import (
|
||||
. "github.com/luispater/CLIProxyAPI/internal/constant"
|
||||
"github.com/luispater/CLIProxyAPI/internal/interfaces"
|
||||
"github.com/luispater/CLIProxyAPI/internal/translator/translator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
translator.Register(
|
||||
OPENAI,
|
||||
CLAUDE,
|
||||
ConvertOpenAIRequestToClaude,
|
||||
interfaces.TranslateResponse{
|
||||
Stream: ConvertClaudeResponseToOpenAI,
|
||||
NonStream: ConvertClaudeResponseToOpenAINonStream,
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user