mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
Add claude code support
This commit is contained in:
281
internal/translator/claude/gemini/claude_gemini_request.go
Normal file
281
internal/translator/claude/gemini/claude_gemini_request.go
Normal file
@@ -0,0 +1,281 @@
|
||||
// Package gemini provides request translation functionality for Gemini to Anthropic API.
|
||||
// It handles parsing and transforming Gemini API requests into Anthropic API format,
|
||||
// extracting model information, system instructions, message contents, and tool declarations.
|
||||
// The package performs JSON data transformation to ensure compatibility
|
||||
// between Gemini API format and Anthropic API's expected format.
|
||||
package gemini
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/luispater/CLIProxyAPI/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertGeminiRequestToAnthropic parses and transforms a Gemini API request into Anthropic API format.
|
||||
// It extracts the model name, system instruction, message contents, and tool declarations
|
||||
// from the raw JSON request and returns them in the format expected by the Anthropic API.
|
||||
func ConvertGeminiRequestToAnthropic(rawJSON []byte) string {
|
||||
// Base Anthropic API template
|
||||
out := `{"model":"","max_tokens":32000,"messages":[]}`
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Helper for generating tool call IDs in the form: toolu_<alphanum>
|
||||
genToolCallID := func() string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var b strings.Builder
|
||||
// 24 chars random suffix
|
||||
for i := 0; i < 24; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
b.WriteByte(letters[n.Int64()])
|
||||
}
|
||||
return "toolu_" + b.String()
|
||||
}
|
||||
|
||||
// FIFO queue to store tool call IDs for matching with tool results
|
||||
// Gemini uses sequential pairing across possibly multiple in-flight
|
||||
// functionCalls, so we keep a FIFO queue of generated tool IDs and
|
||||
// consume them in order when functionResponses arrive.
|
||||
var pendingToolIDs []string
|
||||
|
||||
// Model mapping
|
||||
if v := root.Get("model"); v.Exists() {
|
||||
modelName := v.String()
|
||||
out, _ = sjson.Set(out, "model", modelName)
|
||||
}
|
||||
|
||||
// Generation config
|
||||
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
||||
if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
|
||||
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
if temp := genConfig.Get("temperature"); temp.Exists() {
|
||||
out, _ = sjson.Set(out, "temperature", temp.Float())
|
||||
}
|
||||
if topP := genConfig.Get("topP"); topP.Exists() {
|
||||
out, _ = sjson.Set(out, "top_p", topP.Float())
|
||||
}
|
||||
if stopSeqs := genConfig.Get("stopSequences"); stopSeqs.Exists() && stopSeqs.IsArray() {
|
||||
var stopSequences []string
|
||||
stopSeqs.ForEach(func(_, value gjson.Result) bool {
|
||||
stopSequences = append(stopSequences, value.String())
|
||||
return true
|
||||
})
|
||||
if len(stopSequences) > 0 {
|
||||
out, _ = sjson.Set(out, "stop_sequences", stopSequences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System instruction -> system field
|
||||
if sysInstr := root.Get("system_instruction"); sysInstr.Exists() {
|
||||
if parts := sysInstr.Get("parts"); parts.Exists() && parts.IsArray() {
|
||||
var systemText strings.Builder
|
||||
parts.ForEach(func(_, part gjson.Result) bool {
|
||||
if text := part.Get("text"); text.Exists() {
|
||||
if systemText.Len() > 0 {
|
||||
systemText.WriteString("\n")
|
||||
}
|
||||
systemText.WriteString(text.String())
|
||||
}
|
||||
return true
|
||||
})
|
||||
if systemText.Len() > 0 {
|
||||
systemMessage := `{"role":"user","content":[{"type":"text","text":""}]}`
|
||||
systemMessage, _ = sjson.Set(systemMessage, "content.0.text", systemText.String())
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", systemMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contents -> messages
|
||||
if contents := root.Get("contents"); contents.Exists() && contents.IsArray() {
|
||||
contents.ForEach(func(_, content gjson.Result) bool {
|
||||
role := content.Get("role").String()
|
||||
if role == "model" {
|
||||
role = "assistant"
|
||||
}
|
||||
|
||||
if role == "function" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
// Create message
|
||||
msg := `{"role":"","content":[]}`
|
||||
msg, _ = sjson.Set(msg, "role", role)
|
||||
|
||||
if parts := content.Get("parts"); parts.Exists() && parts.IsArray() {
|
||||
parts.ForEach(func(_, part gjson.Result) bool {
|
||||
// Text content
|
||||
if text := part.Get("text"); text.Exists() {
|
||||
textContent := `{"type":"text","text":""}`
|
||||
textContent, _ = sjson.Set(textContent, "text", text.String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", textContent)
|
||||
return true
|
||||
}
|
||||
|
||||
// Function call (from model/assistant)
|
||||
if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" {
|
||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
||||
|
||||
// Generate a unique tool ID and enqueue it for later matching
|
||||
// with the corresponding functionResponse
|
||||
toolID := genToolCallID()
|
||||
pendingToolIDs = append(pendingToolIDs, toolID)
|
||||
toolUse, _ = sjson.Set(toolUse, "id", toolID)
|
||||
|
||||
if name := fc.Get("name"); name.Exists() {
|
||||
toolUse, _ = sjson.Set(toolUse, "name", name.String())
|
||||
}
|
||||
if args := fc.Get("args"); args.Exists() {
|
||||
toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)
|
||||
}
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
|
||||
return true
|
||||
}
|
||||
|
||||
// Function response (from user)
|
||||
if fr := part.Get("functionResponse"); fr.Exists() {
|
||||
toolResult := `{"type":"tool_result","tool_use_id":"","content":""}`
|
||||
|
||||
// Attach the oldest queued tool_id to pair the response
|
||||
// with its call. If the queue is empty, generate a new id.
|
||||
var toolID string
|
||||
if len(pendingToolIDs) > 0 {
|
||||
toolID = pendingToolIDs[0]
|
||||
// Pop the first element from the queue
|
||||
pendingToolIDs = pendingToolIDs[1:]
|
||||
} else {
|
||||
// Fallback: generate new ID if no pending tool_use found
|
||||
toolID = genToolCallID()
|
||||
}
|
||||
toolResult, _ = sjson.Set(toolResult, "tool_use_id", toolID)
|
||||
|
||||
// Extract result content
|
||||
if result := fr.Get("response.result"); result.Exists() {
|
||||
toolResult, _ = sjson.Set(toolResult, "content", result.String())
|
||||
} else if response := fr.Get("response"); response.Exists() {
|
||||
toolResult, _ = sjson.Set(toolResult, "content", response.Raw)
|
||||
}
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", toolResult)
|
||||
return true
|
||||
}
|
||||
|
||||
// Image content (inline_data)
|
||||
if inlineData := part.Get("inline_data"); inlineData.Exists() {
|
||||
imageContent := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
||||
if mimeType := inlineData.Get("mime_type"); mimeType.Exists() {
|
||||
imageContent, _ = sjson.Set(imageContent, "source.media_type", mimeType.String())
|
||||
}
|
||||
if data := inlineData.Get("data"); data.Exists() {
|
||||
imageContent, _ = sjson.Set(imageContent, "source.data", data.String())
|
||||
}
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", imageContent)
|
||||
return true
|
||||
}
|
||||
|
||||
// File data
|
||||
if fileData := part.Get("file_data"); fileData.Exists() {
|
||||
// For file data, we'll convert to text content with file info
|
||||
textContent := `{"type":"text","text":""}`
|
||||
fileInfo := "File: " + fileData.Get("file_uri").String()
|
||||
if mimeType := fileData.Get("mime_type"); mimeType.Exists() {
|
||||
fileInfo += " (Type: " + mimeType.String() + ")"
|
||||
}
|
||||
textContent, _ = sjson.Set(textContent, "text", fileInfo)
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", textContent)
|
||||
return true
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Only add message if it has content
|
||||
if contentArray := gjson.Get(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 {
|
||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Tools mapping: Gemini functionDeclarations -> Anthropic tools
|
||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||
var anthropicTools []interface{}
|
||||
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
if funcDecls := tool.Get("functionDeclarations"); funcDecls.Exists() && funcDecls.IsArray() {
|
||||
funcDecls.ForEach(func(_, funcDecl gjson.Result) bool {
|
||||
anthropicTool := `"name":"","description":"","input_schema":{}}`
|
||||
|
||||
if name := funcDecl.Get("name"); name.Exists() {
|
||||
anthropicTool, _ = sjson.Set(anthropicTool, "name", name.String())
|
||||
}
|
||||
if desc := funcDecl.Get("description"); desc.Exists() {
|
||||
anthropicTool, _ = sjson.Set(anthropicTool, "description", desc.String())
|
||||
}
|
||||
if params := funcDecl.Get("parameters"); params.Exists() {
|
||||
// Clean up the parameters schema
|
||||
cleaned := params.Raw
|
||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
||||
cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
||||
anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned)
|
||||
} else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() {
|
||||
// Clean up the parameters schema
|
||||
cleaned := params.Raw
|
||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
||||
cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
||||
anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned)
|
||||
}
|
||||
|
||||
anthropicTools = append(anthropicTools, gjson.Parse(anthropicTool).Value())
|
||||
return true
|
||||
})
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(anthropicTools) > 0 {
|
||||
out, _ = sjson.Set(out, "tools", anthropicTools)
|
||||
}
|
||||
}
|
||||
|
||||
// Tool config
|
||||
if toolConfig := root.Get("tool_config"); toolConfig.Exists() {
|
||||
if funcCalling := toolConfig.Get("function_calling_config"); funcCalling.Exists() {
|
||||
if mode := funcCalling.Get("mode"); mode.Exists() {
|
||||
switch mode.String() {
|
||||
case "AUTO":
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
|
||||
case "NONE":
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "none"})
|
||||
case "ANY":
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stream setting
|
||||
if stream := root.Get("stream"); stream.Exists() {
|
||||
out, _ = sjson.Set(out, "stream", stream.Bool())
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "stream", false)
|
||||
}
|
||||
|
||||
var pathsToLower []string
|
||||
toolsResult := gjson.Get(out, "tools")
|
||||
util.Walk(toolsResult, "", "type", &pathsToLower)
|
||||
for _, p := range pathsToLower {
|
||||
fullPath := fmt.Sprintf("tools.%s", p)
|
||||
out, _ = sjson.Set(out, fullPath, strings.ToLower(gjson.Get(out, fullPath).String()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
555
internal/translator/claude/gemini/claude_gemini_response.go
Normal file
555
internal/translator/claude/gemini/claude_gemini_response.go
Normal file
@@ -0,0 +1,555 @@
|
||||
// Package gemini provides response translation functionality for Anthropic to Gemini API.
|
||||
// This package handles the conversion of Anthropic 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 (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertAnthropicResponseToGeminiParams holds parameters for response conversion
|
||||
// It also carries minimal streaming state across calls to assemble tool_use input_json_delta.
|
||||
type ConvertAnthropicResponseToGeminiParams struct {
|
||||
Model string
|
||||
CreatedAt int64
|
||||
ResponseID string
|
||||
LastStorageOutput string
|
||||
IsStreaming bool
|
||||
|
||||
// Streaming state for tool_use assembly
|
||||
// Keyed by content_block index from Claude SSE events
|
||||
ToolUseNames map[int]string // function/tool name per block index
|
||||
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 {
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
// Base Gemini response template
|
||||
template := `{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`
|
||||
|
||||
// Set model version
|
||||
if param.Model != "" {
|
||||
// Map Claude model names back to Gemini model names
|
||||
template, _ = sjson.Set(template, "modelVersion", param.Model)
|
||||
}
|
||||
|
||||
// Set response ID and creation time
|
||||
if param.ResponseID != "" {
|
||||
template, _ = sjson.Set(template, "responseId", param.ResponseID)
|
||||
}
|
||||
|
||||
// Set creation time to current time if not provided
|
||||
if param.CreatedAt == 0 {
|
||||
param.CreatedAt = time.Now().Unix()
|
||||
}
|
||||
template, _ = sjson.Set(template, "createTime", time.Unix(param.CreatedAt, 0).Format(time.RFC3339Nano))
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Initialize response with message metadata
|
||||
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)
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "content_block_start":
|
||||
// Start of a content block - record tool_use name by index for functionCall
|
||||
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 name := cb.Get("name"); name.Exists() {
|
||||
param.ToolUseNames[idx] = name.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "content_block_delta":
|
||||
// Handle content delta (text, thinking, or tool use)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
// Regular text content delta
|
||||
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
|
||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||
thinkingPart := `{"thought":true,"text":""}`
|
||||
thinkingPart, _ = sjson.Set(thinkingPart, "text", text.String())
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", thinkingPart)
|
||||
}
|
||||
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{}
|
||||
}
|
||||
b, ok := param.ToolUseArgs[idx]
|
||||
if !ok || b == nil {
|
||||
bb := &strings.Builder{}
|
||||
param.ToolUseArgs[idx] = bb
|
||||
b = bb
|
||||
}
|
||||
if pj := delta.Get("partial_json"); pj.Exists() {
|
||||
b.WriteString(pj.String())
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "content_block_stop":
|
||||
// End of content block - finalize tool calls if any
|
||||
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 != "" {
|
||||
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
||||
if name != "" {
|
||||
functionCall, _ = sjson.Set(functionCall, "functionCall.name", name)
|
||||
}
|
||||
if argsTrim != "" {
|
||||
functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsTrim)
|
||||
}
|
||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", functionCall)
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
param.LastStorageOutput = template
|
||||
// cleanup used state for this index
|
||||
if param.ToolUseArgs != nil {
|
||||
delete(param.ToolUseArgs, idx)
|
||||
}
|
||||
if param.ToolUseNames != nil {
|
||||
delete(param.ToolUseNames, idx)
|
||||
}
|
||||
return []string{template}
|
||||
}
|
||||
return []string{}
|
||||
|
||||
case "message_delta":
|
||||
// Handle message-level changes (like stop reason)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
if stopReason := delta.Get("stop_reason"); stopReason.Exists() {
|
||||
switch stopReason.String() {
|
||||
case "end_turn":
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
case "tool_use":
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
case "max_tokens":
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "MAX_TOKENS")
|
||||
case "stop_sequence":
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
default:
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
// Basic token counts
|
||||
inputTokens := usage.Get("input_tokens").Int()
|
||||
outputTokens := usage.Get("output_tokens").Int()
|
||||
|
||||
// Set basic usage metadata according to Gemini API specification
|
||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
||||
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)
|
||||
if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() {
|
||||
template, _ = sjson.Set(template, "usageMetadata.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()
|
||||
template, _ = sjson.Set(template, "usageMetadata.cachedContentTokenCount", totalCacheTokens)
|
||||
}
|
||||
|
||||
// Add thinking tokens if present (for models with reasoning capabilities)
|
||||
if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() {
|
||||
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", thinkingTokens.Int())
|
||||
}
|
||||
|
||||
// Set traffic type (required by Gemini API)
|
||||
template, _ = sjson.Set(template, "usageMetadata.trafficType", "PROVISIONED_THROUGHPUT")
|
||||
}
|
||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
||||
|
||||
return []string{template}
|
||||
case "message_stop":
|
||||
// Final message with usage information
|
||||
return []string{}
|
||||
case "error":
|
||||
// Handle error responses
|
||||
errorMsg := root.Get("error.message").String()
|
||||
if errorMsg == "" {
|
||||
errorMsg = "Unknown error occurred"
|
||||
}
|
||||
|
||||
// Create error response in Gemini format
|
||||
errorResponse := `{"error":{"code":400,"message":"","status":"INVALID_ARGUMENT"}}`
|
||||
errorResponse, _ = sjson.Set(errorResponse, "error.message", errorMsg)
|
||||
return []string{errorResponse}
|
||||
|
||||
default:
|
||||
// Unknown event type, return empty
|
||||
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 := "[]"
|
||||
for _, item := range arr {
|
||||
switch itemData := item.(type) {
|
||||
case map[string]interface{}:
|
||||
itemJSON := convertMapToJSON(itemData)
|
||||
result, _ = sjson.SetRaw(result, "-1", itemJSON)
|
||||
case string:
|
||||
result, _ = sjson.Set(result, "-1", itemData)
|
||||
case bool:
|
||||
result, _ = sjson.Set(result, "-1", itemData)
|
||||
case float64, int, int64:
|
||||
result, _ = sjson.Set(result, "-1", itemData)
|
||||
default:
|
||||
result, _ = sjson.Set(result, "-1", itemData)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// convertMapToJSON converts map[string]interface{} to JSON object string
|
||||
func convertMapToJSON(m map[string]interface{}) string {
|
||||
result := "{}"
|
||||
for key, value := range m {
|
||||
switch val := value.(type) {
|
||||
case map[string]interface{}:
|
||||
nestedJSON := convertMapToJSON(val)
|
||||
result, _ = sjson.SetRaw(result, key, nestedJSON)
|
||||
case []interface{}:
|
||||
arrayJSON := convertArrayToJSON(val)
|
||||
result, _ = sjson.SetRaw(result, key, arrayJSON)
|
||||
case string:
|
||||
result, _ = sjson.Set(result, key, val)
|
||||
case bool:
|
||||
result, _ = sjson.Set(result, key, val)
|
||||
case float64, int, int64:
|
||||
result, _ = sjson.Set(result, key, val)
|
||||
default:
|
||||
result, _ = sjson.Set(result, key, val)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
289
internal/translator/claude/openai/claude_openai_request.go
Normal file
289
internal/translator/claude/openai/claude_openai_request.go
Normal file
@@ -0,0 +1,289 @@
|
||||
// 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,
|
||||
// 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.
|
||||
package openai
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertOpenAIRequestToAnthropic parses and transforms an OpenAI Chat Completions API request into Anthropic API format.
|
||||
// It extracts the model name, system instruction, message contents, and tool declarations
|
||||
// from the raw JSON request and returns them in the format expected by the Anthropic API.
|
||||
func ConvertOpenAIRequestToAnthropic(rawJSON []byte) string {
|
||||
// Base Anthropic API template
|
||||
out := `{"model":"","max_tokens":32000,"messages":[]}`
|
||||
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
|
||||
// Helper for generating tool call IDs in the form: toolu_<alphanum>
|
||||
genToolCallID := func() string {
|
||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
var b strings.Builder
|
||||
// 24 chars random suffix
|
||||
for i := 0; i < 24; i++ {
|
||||
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
|
||||
b.WriteByte(letters[n.Int64()])
|
||||
}
|
||||
return "toolu_" + b.String()
|
||||
}
|
||||
|
||||
// Model mapping
|
||||
if model := root.Get("model"); model.Exists() {
|
||||
modelStr := model.String()
|
||||
out, _ = sjson.Set(out, "model", modelStr)
|
||||
}
|
||||
|
||||
// Max tokens
|
||||
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
|
||||
out, _ = sjson.Set(out, "max_tokens", maxTokens.Int())
|
||||
}
|
||||
|
||||
// Temperature
|
||||
if temp := root.Get("temperature"); temp.Exists() {
|
||||
out, _ = sjson.Set(out, "temperature", temp.Float())
|
||||
}
|
||||
|
||||
// Top P
|
||||
if topP := root.Get("top_p"); topP.Exists() {
|
||||
out, _ = sjson.Set(out, "top_p", topP.Float())
|
||||
}
|
||||
|
||||
// Stop sequences
|
||||
if stop := root.Get("stop"); stop.Exists() {
|
||||
if stop.IsArray() {
|
||||
var stopSequences []string
|
||||
stop.ForEach(func(_, value gjson.Result) bool {
|
||||
stopSequences = append(stopSequences, value.String())
|
||||
return true
|
||||
})
|
||||
if len(stopSequences) > 0 {
|
||||
out, _ = sjson.Set(out, "stop_sequences", stopSequences)
|
||||
}
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "stop_sequences", []string{stop.String()})
|
||||
}
|
||||
}
|
||||
|
||||
// Stream
|
||||
if stream := root.Get("stream"); stream.Exists() {
|
||||
out, _ = sjson.Set(out, "stream", stream.Bool())
|
||||
}
|
||||
|
||||
// Process messages
|
||||
var anthropicMessages []interface{}
|
||||
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
||||
|
||||
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
||||
messages.ForEach(func(_, message gjson.Result) bool {
|
||||
role := message.Get("role").String()
|
||||
contentResult := message.Get("content")
|
||||
|
||||
switch role {
|
||||
case "system", "user", "assistant":
|
||||
// Create Anthropic message
|
||||
if role == "system" {
|
||||
role = "user"
|
||||
}
|
||||
|
||||
msg := map[string]interface{}{
|
||||
"role": role,
|
||||
"content": []interface{}{},
|
||||
}
|
||||
|
||||
// Handle content
|
||||
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
||||
// Simple text content
|
||||
msg["content"] = []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "text",
|
||||
"text": contentResult.String(),
|
||||
},
|
||||
}
|
||||
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||
// Array of content parts
|
||||
var contentParts []interface{}
|
||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||
partType := part.Get("type").String()
|
||||
|
||||
switch partType {
|
||||
case "text":
|
||||
contentParts = append(contentParts, map[string]interface{}{
|
||||
"type": "text",
|
||||
"text": part.Get("text").String(),
|
||||
})
|
||||
|
||||
case "image_url":
|
||||
// Convert OpenAI image format to Anthropic format
|
||||
imageURL := part.Get("image_url.url").String()
|
||||
if strings.HasPrefix(imageURL, "data:") {
|
||||
// Extract base64 data and media type
|
||||
parts := strings.Split(imageURL, ",")
|
||||
if len(parts) == 2 {
|
||||
mediaTypePart := strings.Split(parts[0], ";")[0]
|
||||
mediaType := strings.TrimPrefix(mediaTypePart, "data:")
|
||||
data := parts[1]
|
||||
|
||||
contentParts = append(contentParts, map[string]interface{}{
|
||||
"type": "image",
|
||||
"source": map[string]interface{}{
|
||||
"type": "base64",
|
||||
"media_type": mediaType,
|
||||
"data": data,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(contentParts) > 0 {
|
||||
msg["content"] = contentParts
|
||||
}
|
||||
} else {
|
||||
// Initialize empty content array for tool calls
|
||||
msg["content"] = []interface{}{}
|
||||
}
|
||||
|
||||
// Handle tool calls (for assistant messages)
|
||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() && role == "assistant" {
|
||||
var contentParts []interface{}
|
||||
|
||||
// Add existing text content if any
|
||||
if existingContent, ok := msg["content"].([]interface{}); ok {
|
||||
contentParts = existingContent
|
||||
}
|
||||
|
||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||
if toolCall.Get("type").String() == "function" {
|
||||
toolCallID := toolCall.Get("id").String()
|
||||
if toolCallID == "" {
|
||||
toolCallID = genToolCallID()
|
||||
}
|
||||
toolCallIDs = append(toolCallIDs, toolCallID)
|
||||
|
||||
function := toolCall.Get("function")
|
||||
toolUse := map[string]interface{}{
|
||||
"type": "tool_use",
|
||||
"id": toolCallID,
|
||||
"name": function.Get("name").String(),
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
if args := function.Get("arguments"); args.Exists() {
|
||||
argsStr := args.String()
|
||||
if argsStr != "" {
|
||||
var argsMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(argsStr), &argsMap); err == nil {
|
||||
toolUse["input"] = argsMap
|
||||
} else {
|
||||
toolUse["input"] = map[string]interface{}{}
|
||||
}
|
||||
} else {
|
||||
toolUse["input"] = map[string]interface{}{}
|
||||
}
|
||||
} else {
|
||||
toolUse["input"] = map[string]interface{}{}
|
||||
}
|
||||
|
||||
contentParts = append(contentParts, toolUse)
|
||||
}
|
||||
return true
|
||||
})
|
||||
msg["content"] = contentParts
|
||||
}
|
||||
|
||||
anthropicMessages = append(anthropicMessages, msg)
|
||||
|
||||
case "tool":
|
||||
// Handle tool result messages
|
||||
toolCallID := message.Get("tool_call_id").String()
|
||||
content := message.Get("content").String()
|
||||
|
||||
// Create tool result message
|
||||
msg := map[string]interface{}{
|
||||
"role": "user",
|
||||
"content": []interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": toolCallID,
|
||||
"content": content,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
anthropicMessages = append(anthropicMessages, msg)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// Set messages
|
||||
if len(anthropicMessages) > 0 {
|
||||
messagesJSON, _ := json.Marshal(anthropicMessages)
|
||||
out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
|
||||
}
|
||||
|
||||
// Tools mapping: OpenAI tools -> Anthropic tools
|
||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||
var anthropicTools []interface{}
|
||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||
if tool.Get("type").String() == "function" {
|
||||
function := tool.Get("function")
|
||||
anthropicTool := map[string]interface{}{
|
||||
"name": function.Get("name").String(),
|
||||
"description": function.Get("description").String(),
|
||||
}
|
||||
|
||||
// Convert parameters schema
|
||||
if parameters := function.Get("parameters"); parameters.Exists() {
|
||||
anthropicTool["input_schema"] = parameters.Value()
|
||||
}
|
||||
|
||||
anthropicTools = append(anthropicTools, anthropicTool)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
if len(anthropicTools) > 0 {
|
||||
toolsJSON, _ := json.Marshal(anthropicTools)
|
||||
out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
|
||||
}
|
||||
}
|
||||
|
||||
// Tool choice mapping
|
||||
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
|
||||
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
|
||||
if toolChoice.Get("type").String() == "function" {
|
||||
functionName := toolChoice.Get("function.name").String()
|
||||
out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{
|
||||
"type": "tool",
|
||||
"name": functionName,
|
||||
})
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
395
internal/translator/claude/openai/claude_openai_response.go
Normal file
395
internal/translator/claude/openai/claude_openai_response.go
Normal file
@@ -0,0 +1,395 @@
|
||||
// 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
|
||||
// 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.
|
||||
package openai
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// ConvertAnthropicResponseToOpenAIParams holds parameters for response conversion
|
||||
type ConvertAnthropicResponseToOpenAIParams struct {
|
||||
CreatedAt int64
|
||||
ResponseID string
|
||||
FinishReason string
|
||||
// Tool calls accumulator for streaming
|
||||
ToolCallsAccumulator map[int]*ToolCallAccumulator
|
||||
}
|
||||
|
||||
// ToolCallAccumulator holds the state for accumulating tool call data
|
||||
type ToolCallAccumulator struct {
|
||||
ID string
|
||||
Name string
|
||||
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 {
|
||||
root := gjson.ParseBytes(rawJSON)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
// Base OpenAI streaming response template
|
||||
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.CreatedAt > 0 {
|
||||
template, _ = sjson.Set(template, "created", param.CreatedAt)
|
||||
}
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
// Initialize response with message metadata
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
param.ResponseID = message.Get("id").String()
|
||||
param.CreatedAt = time.Now().Unix()
|
||||
|
||||
template, _ = sjson.Set(template, "id", param.ResponseID)
|
||||
template, _ = sjson.Set(template, "model", modelName)
|
||||
template, _ = sjson.Set(template, "created", param.CreatedAt)
|
||||
|
||||
// Set initial role
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
|
||||
// Initialize tool calls accumulator
|
||||
if param.ToolCallsAccumulator == nil {
|
||||
param.ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "content_block_start":
|
||||
// Start of a content block
|
||||
if contentBlock := root.Get("content_block"); contentBlock.Exists() {
|
||||
blockType := contentBlock.Get("type").String()
|
||||
|
||||
if blockType == "tool_use" {
|
||||
// Start of tool call - initialize accumulator
|
||||
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)
|
||||
}
|
||||
|
||||
param.ToolCallsAccumulator[index] = &ToolCallAccumulator{
|
||||
ID: toolCallID,
|
||||
Name: toolName,
|
||||
}
|
||||
|
||||
// Don't output anything yet - wait for complete tool call
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "content_block_delta":
|
||||
// Handle content delta (text or tool use)
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
// Text content delta
|
||||
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
|
||||
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 {
|
||||
accumulator.Arguments.WriteString(partialJSON.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't output anything yet - wait for complete tool call
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
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
|
||||
arguments := accumulator.Arguments.String()
|
||||
if arguments == "" {
|
||||
arguments = "{}"
|
||||
}
|
||||
|
||||
toolCall := map[string]interface{}{
|
||||
"index": index,
|
||||
"id": accumulator.ID,
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": accumulator.Name,
|
||||
"arguments": arguments,
|
||||
},
|
||||
}
|
||||
|
||||
template, _ = sjson.Set(template, "choices.0.delta.tool_calls", []interface{}{toolCall})
|
||||
|
||||
// Clean up the accumulator for this index
|
||||
delete(param.ToolCallsAccumulator, index)
|
||||
|
||||
return []string{template}
|
||||
}
|
||||
}
|
||||
return []string{}
|
||||
|
||||
case "message_delta":
|
||||
// Handle message-level changes
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle usage information
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
usageObj := map[string]interface{}{
|
||||
"prompt_tokens": usage.Get("input_tokens").Int(),
|
||||
"completion_tokens": usage.Get("output_tokens").Int(),
|
||||
"total_tokens": usage.Get("input_tokens").Int() + usage.Get("output_tokens").Int(),
|
||||
}
|
||||
template, _ = sjson.Set(template, "usage", usageObj)
|
||||
}
|
||||
return []string{template}
|
||||
|
||||
case "message_stop":
|
||||
// Final message - send [DONE]
|
||||
return []string{"[DONE]\n"}
|
||||
|
||||
case "ping":
|
||||
// Ping events - ignore
|
||||
return []string{}
|
||||
|
||||
case "error":
|
||||
// Error event
|
||||
if errorData := root.Get("error"); errorData.Exists() {
|
||||
errorResponse := map[string]interface{}{
|
||||
"error": map[string]interface{}{
|
||||
"message": errorData.Get("message").String(),
|
||||
"type": errorData.Get("type").String(),
|
||||
},
|
||||
}
|
||||
errorJSON, _ := json.Marshal(errorResponse)
|
||||
return []string{string(errorJSON)}
|
||||
}
|
||||
return []string{}
|
||||
|
||||
default:
|
||||
// Unknown event type - ignore
|
||||
return []string{}
|
||||
}
|
||||
}
|
||||
|
||||
// mapAnthropicStopReasonToOpenAI maps Anthropic stop reasons to OpenAI stop reasons
|
||||
func mapAnthropicStopReasonToOpenAI(anthropicReason string) string {
|
||||
switch anthropicReason {
|
||||
case "end_turn":
|
||||
return "stop"
|
||||
case "tool_use":
|
||||
return "tool_calls"
|
||||
case "max_tokens":
|
||||
return "length"
|
||||
case "stop_sequence":
|
||||
return "stop"
|
||||
default:
|
||||
return "stop"
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// 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}}`
|
||||
|
||||
var messageID string
|
||||
var model string
|
||||
var createdAt int64
|
||||
var inputTokens, outputTokens int64
|
||||
var reasoningTokens int64
|
||||
var stopReason string
|
||||
var contentParts []string
|
||||
var reasoningParts []string
|
||||
// Use map to track tool calls by index for proper merging
|
||||
toolCallsMap := make(map[int]map[string]interface{})
|
||||
// Track tool call arguments accumulation
|
||||
toolCallArgsMap := make(map[int]strings.Builder)
|
||||
|
||||
for _, chunk := range chunks {
|
||||
root := gjson.ParseBytes(chunk)
|
||||
eventType := root.Get("type").String()
|
||||
|
||||
switch eventType {
|
||||
case "message_start":
|
||||
if message := root.Get("message"); message.Exists() {
|
||||
messageID = message.Get("id").String()
|
||||
model = message.Get("model").String()
|
||||
createdAt = time.Now().Unix()
|
||||
if usage := message.Get("usage"); usage.Exists() {
|
||||
inputTokens = usage.Get("input_tokens").Int()
|
||||
}
|
||||
}
|
||||
|
||||
case "content_block_start":
|
||||
// Handle different content block types
|
||||
if contentBlock := root.Get("content_block"); contentBlock.Exists() {
|
||||
blockType := contentBlock.Get("type").String()
|
||||
if blockType == "thinking" {
|
||||
// Start of thinking/reasoning content
|
||||
continue
|
||||
} else if blockType == "tool_use" {
|
||||
// Initialize tool call tracking
|
||||
index := int(root.Get("index").Int())
|
||||
toolCallsMap[index] = map[string]interface{}{
|
||||
"id": contentBlock.Get("id").String(),
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": contentBlock.Get("name").String(),
|
||||
"arguments": "",
|
||||
},
|
||||
}
|
||||
// Initialize arguments builder for this tool call
|
||||
toolCallArgsMap[index] = strings.Builder{}
|
||||
}
|
||||
}
|
||||
|
||||
case "content_block_delta":
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
deltaType := delta.Get("type").String()
|
||||
switch deltaType {
|
||||
case "text_delta":
|
||||
if text := delta.Get("text"); text.Exists() {
|
||||
contentParts = append(contentParts, text.String())
|
||||
}
|
||||
case "thinking_delta":
|
||||
// Anthropic thinking content -> OpenAI reasoning content
|
||||
if thinking := delta.Get("thinking"); thinking.Exists() {
|
||||
reasoningParts = append(reasoningParts, thinking.String())
|
||||
}
|
||||
case "input_json_delta":
|
||||
// Accumulate tool call arguments
|
||||
if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
|
||||
index := int(root.Get("index").Int())
|
||||
if builder, exists := toolCallArgsMap[index]; exists {
|
||||
builder.WriteString(partialJSON.String())
|
||||
toolCallArgsMap[index] = builder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "content_block_stop":
|
||||
// Finalize tool call arguments for this index
|
||||
index := int(root.Get("index").Int())
|
||||
if toolCall, exists := toolCallsMap[index]; exists {
|
||||
if builder, argsExists := toolCallArgsMap[index]; argsExists {
|
||||
// Set the accumulated arguments
|
||||
arguments := builder.String()
|
||||
if arguments == "" {
|
||||
arguments = "{}"
|
||||
}
|
||||
toolCall["function"].(map[string]interface{})["arguments"] = arguments
|
||||
}
|
||||
}
|
||||
|
||||
case "message_delta":
|
||||
if delta := root.Get("delta"); delta.Exists() {
|
||||
if sr := delta.Get("stop_reason"); sr.Exists() {
|
||||
stopReason = sr.String()
|
||||
}
|
||||
}
|
||||
if usage := root.Get("usage"); usage.Exists() {
|
||||
outputTokens = usage.Get("output_tokens").Int()
|
||||
// Estimate reasoning tokens from thinking content
|
||||
if len(reasoningParts) > 0 {
|
||||
reasoningTokens = int64(len(strings.Join(reasoningParts, "")) / 4) // Rough estimation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set basic response fields
|
||||
out, _ = sjson.Set(out, "id", messageID)
|
||||
out, _ = sjson.Set(out, "created", createdAt)
|
||||
out, _ = sjson.Set(out, "model", model)
|
||||
|
||||
// Set message content
|
||||
messageContent := strings.Join(contentParts, "")
|
||||
out, _ = sjson.Set(out, "choices.0.message.content", messageContent)
|
||||
|
||||
// Add reasoning content if available (following OpenAI reasoning format)
|
||||
if len(reasoningParts) > 0 {
|
||||
reasoningContent := strings.Join(reasoningParts, "")
|
||||
// Add reasoning as a separate field in the message
|
||||
out, _ = sjson.Set(out, "choices.0.message.reasoning", reasoningContent)
|
||||
}
|
||||
|
||||
// Set tool calls if any
|
||||
if len(toolCallsMap) > 0 {
|
||||
// Convert tool calls map to array, preserving order by index
|
||||
var toolCallsArray []interface{}
|
||||
// Find the maximum index to determine the range
|
||||
maxIndex := -1
|
||||
for index := range toolCallsMap {
|
||||
if index > maxIndex {
|
||||
maxIndex = index
|
||||
}
|
||||
}
|
||||
// Iterate through all possible indices up to maxIndex
|
||||
for i := 0; i <= maxIndex; i++ {
|
||||
if toolCall, exists := toolCallsMap[i]; exists {
|
||||
toolCallsArray = append(toolCallsArray, toolCall)
|
||||
}
|
||||
}
|
||||
if len(toolCallsArray) > 0 {
|
||||
out, _ = sjson.Set(out, "choices.0.message.tool_calls", toolCallsArray)
|
||||
out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
||||
}
|
||||
} else {
|
||||
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
||||
}
|
||||
|
||||
// Set usage information
|
||||
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
|
||||
if reasoningTokens > 0 {
|
||||
out, _ = sjson.Set(out, "usage.completion_tokens_details.reasoning_tokens", reasoningTokens)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -7,10 +7,12 @@ package code
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/luispater/CLIProxyAPI/internal/misc"
|
||||
"github.com/luispater/CLIProxyAPI/internal/util"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
@@ -195,5 +197,13 @@ func ConvertGeminiRequestToCodex(rawJSON []byte) string {
|
||||
out, _ = sjson.Set(out, "store", false)
|
||||
out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"})
|
||||
|
||||
var pathsToLower []string
|
||||
toolsResult := gjson.Get(out, "tools")
|
||||
util.Walk(toolsResult, "", "type", &pathsToLower)
|
||||
for _, p := range pathsToLower {
|
||||
fullPath := fmt.Sprintf("tools.%s", p)
|
||||
out, _ = sjson.Set(out, fullPath, strings.ToLower(gjson.Get(out, fullPath).String()))
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -73,70 +73,104 @@ func ConvertOpenAIChatRequestToCodex(rawJSON []byte) string {
|
||||
// }
|
||||
// }
|
||||
|
||||
// Build input from messages, skipping system/tool roles
|
||||
// Build input from messages, handling all message types including tool calls
|
||||
out, _ = sjson.SetRaw(out, "input", `[]`)
|
||||
if messages.IsArray() {
|
||||
arr := messages.Array()
|
||||
for i := 0; i < len(arr); i++ {
|
||||
m := arr[i]
|
||||
role := m.Get("role").String()
|
||||
if role == "tool" || role == "function" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prepare message object
|
||||
msg := `{}`
|
||||
if role == "system" {
|
||||
msg, _ = sjson.Set(msg, "role", "user")
|
||||
} else {
|
||||
msg, _ = sjson.Set(msg, "role", role)
|
||||
}
|
||||
switch role {
|
||||
case "tool":
|
||||
// Handle tool response messages as top-level function_call_output objects
|
||||
toolCallID := m.Get("tool_call_id").String()
|
||||
content := m.Get("content").String()
|
||||
|
||||
msg, _ = sjson.SetRaw(msg, "content", `[]`)
|
||||
// Create function_call_output object
|
||||
funcOutput := `{}`
|
||||
funcOutput, _ = sjson.Set(funcOutput, "type", "function_call_output")
|
||||
funcOutput, _ = sjson.Set(funcOutput, "call_id", toolCallID)
|
||||
funcOutput, _ = sjson.Set(funcOutput, "output", content)
|
||||
out, _ = sjson.SetRaw(out, "input.-1", funcOutput)
|
||||
|
||||
c := m.Get("content")
|
||||
if c.Type == gjson.String {
|
||||
// Single string content
|
||||
partType := "input_text"
|
||||
if role == "assistant" {
|
||||
partType = "output_text"
|
||||
default:
|
||||
// Handle regular messages
|
||||
msg := `{}`
|
||||
msg, _ = sjson.Set(msg, "type", "message")
|
||||
if role == "system" {
|
||||
msg, _ = sjson.Set(msg, "role", "user")
|
||||
} else {
|
||||
msg, _ = sjson.Set(msg, "role", role)
|
||||
}
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", partType)
|
||||
part, _ = sjson.Set(part, "text", c.String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
} else if c.IsArray() {
|
||||
items := c.Array()
|
||||
for j := 0; j < len(items); j++ {
|
||||
it := items[j]
|
||||
t := it.Get("type").String()
|
||||
switch t {
|
||||
case "text":
|
||||
partType := "input_text"
|
||||
if role == "assistant" {
|
||||
partType = "output_text"
|
||||
}
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", partType)
|
||||
part, _ = sjson.Set(part, "text", it.Get("text").String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
case "image_url":
|
||||
// Map image inputs to input_image for Responses API
|
||||
if role == "user" {
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", "input_image")
|
||||
if u := it.Get("image_url.url"); u.Exists() {
|
||||
part, _ = sjson.Set(part, "image_url", u.String())
|
||||
|
||||
msg, _ = sjson.SetRaw(msg, "content", `[]`)
|
||||
|
||||
// Handle regular content
|
||||
c := m.Get("content")
|
||||
if c.Exists() && c.Type == gjson.String && c.String() != "" {
|
||||
// Single string content
|
||||
partType := "input_text"
|
||||
if role == "assistant" {
|
||||
partType = "output_text"
|
||||
}
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", partType)
|
||||
part, _ = sjson.Set(part, "text", c.String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
} else if c.Exists() && c.IsArray() {
|
||||
items := c.Array()
|
||||
for j := 0; j < len(items); j++ {
|
||||
it := items[j]
|
||||
t := it.Get("type").String()
|
||||
switch t {
|
||||
case "text":
|
||||
partType := "input_text"
|
||||
if role == "assistant" {
|
||||
partType = "output_text"
|
||||
}
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", partType)
|
||||
part, _ = sjson.Set(part, "text", it.Get("text").String())
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
case "image_url":
|
||||
// Map image inputs to input_image for Responses API
|
||||
if role == "user" {
|
||||
part := `{}`
|
||||
part, _ = sjson.Set(part, "type", "input_image")
|
||||
if u := it.Get("image_url.url"); u.Exists() {
|
||||
part, _ = sjson.Set(part, "image_url", u.String())
|
||||
}
|
||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
||||
}
|
||||
case "file":
|
||||
// Files are not specified in examples; skip for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
||||
|
||||
// Handle tool calls for assistant messages as separate top-level objects
|
||||
if role == "assistant" {
|
||||
toolCalls := m.Get("tool_calls")
|
||||
if toolCalls.Exists() && toolCalls.IsArray() {
|
||||
toolCallsArr := toolCalls.Array()
|
||||
for j := 0; j < len(toolCallsArr); j++ {
|
||||
tc := toolCallsArr[j]
|
||||
if tc.Get("type").String() == "function" {
|
||||
// Create function_call as top-level object
|
||||
funcCall := `{}`
|
||||
funcCall, _ = sjson.Set(funcCall, "type", "function_call")
|
||||
funcCall, _ = sjson.Set(funcCall, "call_id", tc.Get("id").String())
|
||||
funcCall, _ = sjson.Set(funcCall, "name", tc.Get("function.name").String())
|
||||
funcCall, _ = sjson.Set(funcCall, "arguments", tc.Get("function.arguments").String())
|
||||
out, _ = sjson.SetRaw(out, "input.-1", funcCall)
|
||||
}
|
||||
}
|
||||
case "file":
|
||||
// Files are not specified in examples; skip for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,9 +78,9 @@ func ConvertCliResponseToClaudeCode(rawJSON []byte, isGlAPIKey, hasFirstResponse
|
||||
// First, close any existing content block
|
||||
if *responseType != 0 {
|
||||
if *responseType == 2 {
|
||||
output = output + "event: content_block_delta\n"
|
||||
output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, *responseIndex)
|
||||
output = output + "\n\n\n"
|
||||
// output = output + "event: content_block_delta\n"
|
||||
// output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, *responseIndex)
|
||||
// output = output + "\n\n\n"
|
||||
}
|
||||
output = output + "event: content_block_stop\n"
|
||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, *responseIndex)
|
||||
@@ -109,9 +109,9 @@ func ConvertCliResponseToClaudeCode(rawJSON []byte, isGlAPIKey, hasFirstResponse
|
||||
// First, close any existing content block
|
||||
if *responseType != 0 {
|
||||
if *responseType == 2 {
|
||||
output = output + "event: content_block_delta\n"
|
||||
output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, *responseIndex)
|
||||
output = output + "\n\n\n"
|
||||
// output = output + "event: content_block_delta\n"
|
||||
// output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, *responseIndex)
|
||||
// output = output + "\n\n\n"
|
||||
}
|
||||
output = output + "event: content_block_stop\n"
|
||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, *responseIndex)
|
||||
@@ -147,9 +147,9 @@ func ConvertCliResponseToClaudeCode(rawJSON []byte, isGlAPIKey, hasFirstResponse
|
||||
|
||||
// Special handling for thinking state transition
|
||||
if *responseType == 2 {
|
||||
output = output + "event: content_block_delta\n"
|
||||
output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, *responseIndex)
|
||||
output = output + "\n\n\n"
|
||||
// output = output + "event: content_block_delta\n"
|
||||
// output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, *responseIndex)
|
||||
// output = output + "\n\n\n"
|
||||
}
|
||||
|
||||
// Close any other existing content block
|
||||
|
||||
@@ -45,11 +45,40 @@ func ConvertOpenAIChatRequestToCli(rawJSON []byte) (string, *client.Content, []c
|
||||
var systemInstruction *client.Content
|
||||
messagesResult := gjson.GetBytes(rawJSON, "messages")
|
||||
|
||||
// Pre-process tool responses to create a lookup map
|
||||
// This first pass collects all tool responses so they can be matched with their corresponding calls
|
||||
// Pre-process messages to create mappings for tool calls and responses
|
||||
// First pass: collect function call ID to function name mappings
|
||||
toolCallToFunctionName := make(map[string]string)
|
||||
toolItems := make(map[string]*client.FunctionResponse)
|
||||
|
||||
if messagesResult.IsArray() {
|
||||
messagesResults := messagesResult.Array()
|
||||
|
||||
// First pass: collect function call mappings
|
||||
for i := 0; i < len(messagesResults); i++ {
|
||||
messageResult := messagesResults[i]
|
||||
roleResult := messageResult.Get("role")
|
||||
if roleResult.Type != gjson.String {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract function call ID to function name mappings
|
||||
if roleResult.String() == "assistant" {
|
||||
toolCallsResult := messageResult.Get("tool_calls")
|
||||
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
||||
tcsResult := toolCallsResult.Array()
|
||||
for j := 0; j < len(tcsResult); j++ {
|
||||
tcResult := tcsResult[j]
|
||||
if tcResult.Get("type").String() == "function" {
|
||||
functionID := tcResult.Get("id").String()
|
||||
functionName := tcResult.Get("function.name").String()
|
||||
toolCallToFunctionName[functionID] = functionName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: collect tool responses with correct function names
|
||||
for i := 0; i < len(messagesResults); i++ {
|
||||
messageResult := messagesResults[i]
|
||||
roleResult := messageResult.Get("role")
|
||||
@@ -70,14 +99,15 @@ func ConvertOpenAIChatRequestToCli(rawJSON []byte) (string, *client.Content, []c
|
||||
responseData = contentResult.Get("text").String()
|
||||
}
|
||||
|
||||
// Clean up tool call ID by removing timestamp suffix
|
||||
// This normalizes IDs for consistent matching between calls and responses
|
||||
toolCallIDs := strings.Split(toolCallID, "-")
|
||||
strings.Join(toolCallIDs, "-")
|
||||
newToolCallID := strings.Join(toolCallIDs[:len(toolCallIDs)-1], "-")
|
||||
// Get the correct function name from the mapping
|
||||
functionName := toolCallToFunctionName[toolCallID]
|
||||
if functionName == "" {
|
||||
// Fallback: use tool call ID if function name not found
|
||||
functionName = toolCallID
|
||||
}
|
||||
|
||||
// Create function response object with normalized ID and response data
|
||||
functionResponse := client.FunctionResponse{Name: newToolCallID, Response: map[string]interface{}{"result": responseData}}
|
||||
// Create function response object with correct function name
|
||||
functionResponse := client.FunctionResponse{Name: functionName, Response: map[string]interface{}{"result": responseData}}
|
||||
toolItems[toolCallID] = &functionResponse
|
||||
}
|
||||
}
|
||||
@@ -94,9 +124,10 @@ func ConvertOpenAIChatRequestToCli(rawJSON []byte) (string, *client.Content, []c
|
||||
continue
|
||||
}
|
||||
|
||||
switch roleResult.String() {
|
||||
// System messages are converted to a user message followed by a model's acknowledgment.
|
||||
case "system":
|
||||
role := roleResult.String()
|
||||
|
||||
if role == "system" && len(messagesResults) > 1 {
|
||||
// System messages are converted to a user message followed by a model's acknowledgment.
|
||||
if contentResult.Type == gjson.String {
|
||||
systemInstruction = &client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}}
|
||||
} else if contentResult.IsObject() {
|
||||
@@ -105,8 +136,8 @@ func ConvertOpenAIChatRequestToCli(rawJSON []byte) (string, *client.Content, []c
|
||||
systemInstruction = &client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.Get("text").String()}}}
|
||||
}
|
||||
}
|
||||
// User messages can contain simple text or a multi-part body.
|
||||
case "user":
|
||||
} else if role == "user" || (role == "system" && len(messagesResults) == 1) { // If there's only a system message, treat it as a user message.
|
||||
// User messages can contain simple text or a multi-part body.
|
||||
if contentResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}})
|
||||
} else if contentResult.IsArray() {
|
||||
@@ -151,9 +182,10 @@ func ConvertOpenAIChatRequestToCli(rawJSON []byte) (string, *client.Content, []c
|
||||
}
|
||||
contents = append(contents, client.Content{Role: "user", Parts: parts})
|
||||
}
|
||||
// Assistant messages can contain text responses or tool calls
|
||||
// In the internal format, assistant messages are converted to "model" role
|
||||
case "assistant":
|
||||
} else if role == "assistant" {
|
||||
// Assistant messages can contain text responses or tool calls
|
||||
// In the internal format, assistant messages are converted to "model" role
|
||||
|
||||
if contentResult.Type == gjson.String {
|
||||
// Simple text response from the assistant
|
||||
contents = append(contents, client.Content{Role: "model", Parts: []client.Part{{Text: contentResult.String()}}})
|
||||
|
||||
@@ -101,7 +101,7 @@ func ConvertCliResponseToOpenAIChat(rawJSON []byte, unixTimestamp int64, isGlAPI
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", fcArgsResult.Raw)
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", functionCallTemplate)
|
||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user