diff --git a/README.md b/README.md
index 03121c7b..50c750a6 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,17 @@ GLM CODING PLAN is a subscription service designed for AI coding, starting at ju
Get 10% OFF GLM CODING PLAN:https://z.ai/subscribe?ic=8JVLJQFSKB
+---
+
+
+
+
+ |
+Thanks to PackyCode for sponsoring this project! PackyCode is a reliable and efficient API relay service provider, offering relay services for Claude Code, Codex, Gemini, and more. PackyCode provides special discounts for our software users: register using this link and enter the "cliproxyapi" promo code during recharge to get 10% off. |
+
+
+
+
## Overview
- OpenAI/Gemini/Claude compatible API endpoints for CLI models
diff --git a/README_CN.md b/README_CN.md
index 4624e88f..d4ab8eec 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -18,6 +18,17 @@ GLM CODING PLAN 是专为AI编码打造的订阅套餐,每月最低仅需20元
智谱AI为本软件提供了特别优惠,使用以下链接购买可以享受九折优惠:https://www.bigmodel.cn/claude-code?ic=RRVJPB5SII
+---
+
+
+
+
+ |
+感谢 PackyCode 对本项目的赞助!PackyCode 是一家可靠高效的 API 中转服务商,提供 Claude Code、Codex、Gemini 等多种服务的中转。PackyCode 为本软件用户提供了特别优惠:使用此链接注册,并在充值时输入 "cliproxyapi" 优惠码即可享受九折优惠。 |
+
+
+
+
## 功能特性
- 为 CLI 模型提供 OpenAI/Gemini/Claude/Codex 兼容的 API 端点
diff --git a/assets/packycode.png b/assets/packycode.png
new file mode 100644
index 00000000..4fc7eecc
Binary files /dev/null and b/assets/packycode.png differ
diff --git a/config.example.yaml b/config.example.yaml
index 1e084cb4..89385c8f 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -66,6 +66,10 @@ quota-exceeded:
switch-project: true # Whether to automatically switch to another project when a quota is exceeded
switch-preview-model: true # Whether to automatically switch to a preview model when a quota is exceeded
+# Routing strategy for selecting credentials when multiple match.
+routing:
+ strategy: "round-robin" # round-robin (default), fill-first
+
# When true, enable authentication for the WebSocket API (/v1/ws).
ws-auth: false
diff --git a/internal/config/config.go b/internal/config/config.go
index cd56bd77..6bd74c03 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -60,6 +60,9 @@ type Config struct {
// QuotaExceeded defines the behavior when a quota is exceeded.
QuotaExceeded QuotaExceeded `yaml:"quota-exceeded" json:"quota-exceeded"`
+ // Routing controls credential selection behavior.
+ Routing RoutingConfig `yaml:"routing" json:"routing"`
+
// WebsocketAuth enables or disables authentication for the WebSocket API.
WebsocketAuth bool `yaml:"ws-auth" json:"ws-auth"`
@@ -124,6 +127,13 @@ type QuotaExceeded struct {
SwitchPreviewModel bool `yaml:"switch-preview-model" json:"switch-preview-model"`
}
+// RoutingConfig configures how credentials are selected for requests.
+type RoutingConfig struct {
+ // Strategy selects the credential selection strategy.
+ // Supported values: "round-robin" (default), "fill-first".
+ Strategy string `yaml:"strategy,omitempty" json:"strategy,omitempty"`
+}
+
// AmpModelMapping defines a model name mapping for Amp CLI requests.
// When Amp requests a model that isn't available locally, this mapping
// allows routing to an alternative model that IS available.
diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go
index 203ec321..2b4ec748 100644
--- a/internal/runtime/executor/antigravity_executor.go
+++ b/internal/runtime/executor/antigravity_executor.go
@@ -33,18 +33,18 @@ import (
)
const (
- antigravityBaseURLDaily = "https://daily-cloudcode-pa.sandbox.googleapis.com"
- // antigravityBaseURLAutopush = "https://autopush-cloudcode-pa.sandbox.googleapis.com"
- antigravityBaseURLProd = "https://cloudcode-pa.googleapis.com"
- antigravityCountTokensPath = "/v1internal:countTokens"
- antigravityStreamPath = "/v1internal:streamGenerateContent"
- antigravityGeneratePath = "/v1internal:generateContent"
- antigravityModelsPath = "/v1internal:fetchAvailableModels"
- antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
- antigravityClientSecret = "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"
- defaultAntigravityAgent = "antigravity/1.104.0 darwin/arm64"
- antigravityAuthType = "antigravity"
- refreshSkew = 3000 * time.Second
+ antigravityBaseURLDaily = "https://daily-cloudcode-pa.googleapis.com"
+ antigravitySandboxBaseURLDaily = "https://daily-cloudcode-pa.sandbox.googleapis.com"
+ antigravityBaseURLProd = "https://cloudcode-pa.googleapis.com"
+ antigravityCountTokensPath = "/v1internal:countTokens"
+ antigravityStreamPath = "/v1internal:streamGenerateContent"
+ antigravityGeneratePath = "/v1internal:generateContent"
+ antigravityModelsPath = "/v1internal:fetchAvailableModels"
+ antigravityClientID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com"
+ antigravityClientSecret = "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"
+ defaultAntigravityAgent = "antigravity/1.104.0 darwin/arm64"
+ antigravityAuthType = "antigravity"
+ refreshSkew = 3000 * time.Second
)
var (
@@ -1156,7 +1156,7 @@ func antigravityBaseURLFallbackOrder(auth *cliproxyauth.Auth) []string {
}
return []string{
antigravityBaseURLDaily,
- // antigravityBaseURLAutopush,
+ antigravitySandboxBaseURLDaily,
antigravityBaseURLProd,
}
}
diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go
index 67671026..0c31f424 100644
--- a/internal/runtime/executor/claude_executor.go
+++ b/internal/runtime/executor/claude_executor.go
@@ -662,7 +662,14 @@ func decodeResponseBody(body io.ReadCloser, contentEncoding string) (io.ReadClos
}
func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string, stream bool, extraBetas []string) {
- r.Header.Set("Authorization", "Bearer "+apiKey)
+ useAPIKey := auth != nil && auth.Attributes != nil && strings.TrimSpace(auth.Attributes["api_key"]) != ""
+ isAnthropicBase := r.URL != nil && strings.EqualFold(r.URL.Scheme, "https") && strings.EqualFold(r.URL.Host, "api.anthropic.com")
+ if isAnthropicBase && useAPIKey {
+ r.Header.Del("Authorization")
+ r.Header.Set("x-api-key", apiKey)
+ } else {
+ r.Header.Set("Authorization", "Bearer "+apiKey)
+ }
r.Header.Set("Content-Type", "application/json")
var ginHeaders http.Header
diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go
index e2eeb6e0..2287bccc 100644
--- a/internal/translator/antigravity/claude/antigravity_claude_request.go
+++ b/internal/translator/antigravity/claude/antigravity_claude_request.go
@@ -86,6 +86,10 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
hasSystemInstruction = true
}
}
+ } else if systemResult.Type == gjson.String {
+ systemInstructionJSON = `{"role":"user","parts":[{"text":""}]}`
+ systemInstructionJSON, _ = sjson.Set(systemInstructionJSON, "parts.0.text", systemResult.String())
+ hasSystemInstruction = true
}
// contents
@@ -121,32 +125,31 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
// Use GetThinkingText to handle wrapped thinking objects
thinkingText := util.GetThinkingText(contentResult)
signatureResult := contentResult.Get("signature")
- clientSignature := ""
- if signatureResult.Exists() && signatureResult.String() != "" {
- clientSignature = signatureResult.String()
- }
-
- // Always try cached signature first (more reliable than client-provided)
- // Client may send stale or invalid signatures from different sessions
- signature := ""
- if sessionID != "" && thinkingText != "" {
- if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" {
- signature = cachedSig
- log.Debugf("Using cached signature for thinking block")
+ clientSignature := ""
+ if signatureResult.Exists() && signatureResult.String() != "" {
+ clientSignature = signatureResult.String()
}
- }
- // Fallback to client signature only if cache miss and client signature is valid
- if signature == "" && cache.HasValidSignature(clientSignature) {
- signature = clientSignature
- log.Debugf("Using client-provided signature for thinking block")
- }
+ // Always try cached signature first (more reliable than client-provided)
+ // Client may send stale or invalid signatures from different sessions
+ signature := ""
+ if sessionID != "" && thinkingText != "" {
+ if cachedSig := cache.GetCachedSignature(sessionID, thinkingText); cachedSig != "" {
+ signature = cachedSig
+ log.Debugf("Using cached signature for thinking block")
+ }
+ }
- // Store for subsequent tool_use in the same message
- if cache.HasValidSignature(signature) {
- currentMessageThinkingSignature = signature
- }
+ // Fallback to client signature only if cache miss and client signature is valid
+ if signature == "" && cache.HasValidSignature(clientSignature) {
+ signature = clientSignature
+ log.Debugf("Using client-provided signature for thinking block")
+ }
+ // Store for subsequent tool_use in the same message
+ if cache.HasValidSignature(signature) {
+ currentMessageThinkingSignature = signature
+ }
// Skip trailing unsigned thinking blocks on last assistant message
isUnsigned := !cache.HasValidSignature(signature)
@@ -321,6 +324,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
// tools
toolsJSON := ""
toolDeclCount := 0
+ allowedToolKeys := []string{"name", "description", "behavior", "parameters", "parametersJsonSchema", "response", "responseJsonSchema"}
toolsResult := gjson.GetBytes(rawJSON, "tools")
if toolsResult.IsArray() {
toolsJSON = `[{"functionDeclarations":[]}]`
@@ -333,10 +337,12 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
inputSchema := util.CleanJSONSchemaForAntigravity(inputSchemaResult.Raw)
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
- tool, _ = sjson.Delete(tool, "strict")
- tool, _ = sjson.Delete(tool, "input_examples")
- tool, _ = sjson.Delete(tool, "type")
- tool, _ = sjson.Delete(tool, "cache_control")
+ for toolKey := range gjson.Parse(tool).Map() {
+ if util.InArray(allowedToolKeys, toolKey) {
+ continue
+ }
+ tool, _ = sjson.Delete(tool, toolKey)
+ }
toolsJSON, _ = sjson.SetRaw(toolsJSON, "0.functionDeclarations.-1", tool)
toolDeclCount++
}
diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go
index 5e74b916..80660378 100644
--- a/internal/translator/antigravity/claude/antigravity_claude_response.go
+++ b/internal/translator/antigravity/claude/antigravity_claude_response.go
@@ -471,7 +471,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
toolBlock, _ = sjson.Set(toolBlock, "name", name)
- if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) {
+ if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) && args.IsObject() {
toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)
}
diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_request.go b/internal/translator/antigravity/gemini/antigravity_gemini_request.go
index 394cc05b..a83c177d 100644
--- a/internal/translator/antigravity/gemini/antigravity_gemini_request.go
+++ b/internal/translator/antigravity/gemini/antigravity_gemini_request.go
@@ -7,7 +7,6 @@ package gemini
import (
"bytes"
- "encoding/json"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
@@ -135,41 +134,31 @@ func ConvertGeminiRequestToAntigravity(_ string, inputRawJSON []byte, _ bool) []
// FunctionCallGroup represents a group of function calls and their responses
type FunctionCallGroup struct {
- ModelContent map[string]interface{}
- FunctionCalls []gjson.Result
ResponsesNeeded int
}
-// parseFunctionResponse attempts to unmarshal a function response part.
-// Falls back to gjson extraction if standard json.Unmarshal fails.
-func parseFunctionResponse(response gjson.Result) map[string]interface{} {
- var responseMap map[string]interface{}
- err := json.Unmarshal([]byte(response.Raw), &responseMap)
- if err == nil {
- return responseMap
+// parseFunctionResponseRaw attempts to normalize a function response part into a JSON object string.
+// Falls back to a minimal "functionResponse" object when parsing fails.
+func parseFunctionResponseRaw(response gjson.Result) string {
+ if response.IsObject() && gjson.Valid(response.Raw) {
+ return response.Raw
}
- log.Debugf("unmarshal function response failed, using fallback: %v", err)
+ log.Debugf("parse function response failed, using fallback")
funcResp := response.Get("functionResponse")
if funcResp.Exists() {
- fr := map[string]interface{}{
- "name": funcResp.Get("name").String(),
- "response": map[string]interface{}{
- "result": funcResp.Get("response").String(),
- },
- }
+ fr := `{"functionResponse":{"name":"","response":{"result":""}}}`
+ fr, _ = sjson.Set(fr, "functionResponse.name", funcResp.Get("name").String())
+ fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String())
if id := funcResp.Get("id").String(); id != "" {
- fr["id"] = id
+ fr, _ = sjson.Set(fr, "functionResponse.id", id)
}
- return map[string]interface{}{"functionResponse": fr}
+ return fr
}
- return map[string]interface{}{
- "functionResponse": map[string]interface{}{
- "name": "unknown",
- "response": map[string]interface{}{"result": response.String()},
- },
- }
+ fr := `{"functionResponse":{"name":"unknown","response":{"result":""}}}`
+ fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String())
+ return fr
}
// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
@@ -196,7 +185,7 @@ func fixCLIToolResponse(input string) (string, error) {
}
// Initialize data structures for processing and grouping
- var newContents []interface{} // Final processed contents array
+ contentsWrapper := `{"contents":[]}`
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
var collectedResponses []gjson.Result // Standalone responses to be matched
@@ -228,17 +217,16 @@ func fixCLIToolResponse(input string) (string, error) {
collectedResponses = collectedResponses[group.ResponsesNeeded:]
// Create merged function response content
- var responseParts []interface{}
+ functionResponseContent := `{"parts":[],"role":"function"}`
for _, response := range groupResponses {
- responseParts = append(responseParts, parseFunctionResponse(response))
+ partRaw := parseFunctionResponseRaw(response)
+ if partRaw != "" {
+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
+ }
}
- if len(responseParts) > 0 {
- functionResponseContent := map[string]interface{}{
- "parts": responseParts,
- "role": "function",
- }
- newContents = append(newContents, functionResponseContent)
+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
}
// Remove this group as it's been satisfied
@@ -252,50 +240,42 @@ func fixCLIToolResponse(input string) (string, error) {
// If this is a model with function calls, create a new group
if role == "model" {
- var functionCallsInThisModel []gjson.Result
+ functionCallsCount := 0
parts.ForEach(func(_, part gjson.Result) bool {
if part.Get("functionCall").Exists() {
- functionCallsInThisModel = append(functionCallsInThisModel, part)
+ functionCallsCount++
}
return true
})
- if len(functionCallsInThisModel) > 0 {
+ if functionCallsCount > 0 {
// Add the model content
- var contentMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
+ if !value.IsObject() {
+ log.Warnf("failed to parse model content")
return true
}
- newContents = append(newContents, contentMap)
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
// Create a new group for tracking responses
group := &FunctionCallGroup{
- ModelContent: contentMap,
- FunctionCalls: functionCallsInThisModel,
- ResponsesNeeded: len(functionCallsInThisModel),
+ ResponsesNeeded: functionCallsCount,
}
pendingGroups = append(pendingGroups, group)
} else {
// Regular model content without function calls
- var contentMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
+ if !value.IsObject() {
+ log.Warnf("failed to parse content")
return true
}
- newContents = append(newContents, contentMap)
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
}
} else {
// Non-model content (user, etc.)
- var contentMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
+ if !value.IsObject() {
+ log.Warnf("failed to parse content")
return true
}
- newContents = append(newContents, contentMap)
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
}
return true
@@ -307,25 +287,23 @@ func fixCLIToolResponse(input string) (string, error) {
groupResponses := collectedResponses[:group.ResponsesNeeded]
collectedResponses = collectedResponses[group.ResponsesNeeded:]
- var responseParts []interface{}
+ functionResponseContent := `{"parts":[],"role":"function"}`
for _, response := range groupResponses {
- responseParts = append(responseParts, parseFunctionResponse(response))
+ partRaw := parseFunctionResponseRaw(response)
+ if partRaw != "" {
+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
+ }
}
- if len(responseParts) > 0 {
- functionResponseContent := map[string]interface{}{
- "parts": responseParts,
- "role": "function",
- }
- newContents = append(newContents, functionResponseContent)
+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
}
}
}
// Update the original JSON with the new contents
result := input
- newContentsJSON, _ := json.Marshal(newContents)
- result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
+ result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
return result, nil
}
diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go
index 293add0f..573b8d45 100644
--- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go
+++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go
@@ -192,6 +192,14 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
} else if content.IsObject() && content.Get("type").String() == "text" {
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", content.Get("text").String())
+ } else if content.IsArray() {
+ contents := content.Array()
+ if len(contents) > 0 {
+ out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
+ for j := 0; j < len(contents); j++ {
+ out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", j), contents[j].Get("text").String())
+ }
+ }
}
} else if role == "user" || (role == "system" && len(arr) == 1) {
// Build single user content node to avoid splitting into multiple contents
@@ -258,7 +266,11 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
fargs := tc.Get("function.arguments").String()
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.id", fid)
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.name", fname)
- node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
+ if gjson.Valid(fargs) {
+ node, _ = sjson.SetRawBytes(node, "parts."+itoa(p)+".functionCall.args", []byte(fargs))
+ } else {
+ node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".functionCall.args.params", []byte(fargs))
+ }
node, _ = sjson.SetBytes(node, "parts."+itoa(p)+".thoughtSignature", geminiCLIFunctionThoughtSignature)
p++
if fid != "" {
@@ -319,7 +331,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
continue
}
- fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
if errSet != nil {
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
continue
@@ -334,7 +346,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
continue
}
- fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
if errSet != nil {
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
continue
diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go
index f9f5dea4..7282ebc8 100644
--- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go
+++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go
@@ -8,7 +8,6 @@ package chat_completions
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
"sync/atomic"
@@ -182,21 +181,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
mimeType = "image/png"
}
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
- imagePayload, err := json.Marshal(map[string]any{
- "type": "image_url",
- "image_url": map[string]string{
- "url": imageURL,
- },
- })
- if err != nil {
- continue
- }
+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
imagesResult := gjson.Get(template, "choices.0.delta.images")
if !imagesResult.Exists() || !imagesResult.IsArray() {
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
}
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
- template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
}
}
}
diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go
index 6518947b..faf1f9d1 100644
--- a/internal/translator/claude/gemini/claude_gemini_request.go
+++ b/internal/translator/claude/gemini/claude_gemini_request.go
@@ -194,7 +194,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if name := fc.Get("name"); name.Exists() {
toolUse, _ = sjson.Set(toolUse, "name", name.String())
}
- if args := fc.Get("args"); args.Exists() {
+ if args := fc.Get("args"); args.Exists() && args.IsObject() {
toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)
}
msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
@@ -314,11 +314,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
if mode := funcCalling.Get("mode"); mode.Exists() {
switch mode.String() {
case "AUTO":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
case "NONE":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "none"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"none"}`)
case "ANY":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
}
}
}
diff --git a/internal/translator/claude/gemini/claude_gemini_response.go b/internal/translator/claude/gemini/claude_gemini_response.go
index c77cca4b..c38f8ae7 100644
--- a/internal/translator/claude/gemini/claude_gemini_response.go
+++ b/internal/translator/claude/gemini/claude_gemini_response.go
@@ -263,51 +263,6 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
}
}
-// 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
-}
-
// 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
@@ -356,8 +311,8 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
}
// Process each streaming event and collect parts
- var allParts []interface{}
- var finalUsage map[string]interface{}
+ var allParts []string
+ var finalUsageJSON string
var responseID string
var createdAt int64
@@ -407,16 +362,14 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
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)
+ allParts = append(allParts, partJSON)
}
case "thinking_delta":
// Process reasoning/thinking content
if text := delta.Get("thinking"); 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)
+ allParts = append(allParts, partJSON)
}
case "input_json_delta":
// accumulate args partial_json for this index
@@ -456,9 +409,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
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)
+ allParts = append(allParts, functionCallJSON)
// cleanup used state for this index
if newParam.ToolUseArgs != nil {
delete(newParam.ToolUseArgs, idx)
@@ -501,8 +452,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
// 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{})
+ finalUsageJSON = usageJSON
}
}
}
@@ -520,12 +470,16 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
// Set the consolidated parts array
if len(consolidatedParts) > 0 {
- template, _ = sjson.SetRaw(template, "candidates.0.content.parts", convertToJSONString(consolidatedParts))
+ partsJSON := "[]"
+ for _, partJSON := range consolidatedParts {
+ partsJSON, _ = sjson.SetRaw(partsJSON, "-1", partJSON)
+ }
+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts", partsJSON)
}
// Set usage metadata
- if finalUsage != nil {
- template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage))
+ if finalUsageJSON != "" {
+ template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON)
}
return template
@@ -539,12 +493,12 @@ func GeminiTokenCount(ctx context.Context, count int64) string {
// 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{} {
+func consolidateParts(parts []string) []string {
if len(parts) == 0 {
return parts
}
- var consolidated []interface{}
+ var consolidated []string
var currentTextPart strings.Builder
var currentThoughtPart strings.Builder
var hasText, hasThought bool
@@ -554,8 +508,7 @@ func consolidateParts(parts []interface{}) []interface{} {
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)
+ consolidated = append(consolidated, textPartJSON)
currentTextPart.Reset()
hasText = false
}
@@ -566,42 +519,42 @@ func consolidateParts(parts []interface{}) []interface{} {
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)
+ consolidated = append(consolidated, thoughtPartJSON)
currentThoughtPart.Reset()
hasThought = false
}
}
- for _, part := range parts {
- partMap, ok := part.(map[string]interface{})
- if !ok {
+ for _, partJSON := range parts {
+ part := gjson.Parse(partJSON)
+ if !part.Exists() || !part.IsObject() {
// Flush any pending parts and add this non-text part
flushText()
flushThought()
- consolidated = append(consolidated, part)
+ consolidated = append(consolidated, partJSON)
continue
}
- if thought, isThought := partMap["thought"]; isThought && thought == true {
+ thought := part.Get("thought")
+ if thought.Exists() && thought.Type == gjson.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)
+ if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
+ currentThoughtPart.WriteString(text.String())
hasThought = true
}
- } else if text, hasTextContent := partMap["text"].(string); hasTextContent {
+ } else if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
// This is a regular text part - flush any pending thought first
flushThought() // Flush any pending thought first
- currentTextPart.WriteString(text)
+ currentTextPart.WriteString(text.String())
hasText = true
} else {
// This is some other type of part (like function call) - flush both text and thought
flushText()
flushThought()
- consolidated = append(consolidated, part)
+ consolidated = append(consolidated, partJSON)
}
}
@@ -611,20 +564,3 @@ func consolidateParts(parts []interface{}) []interface{} {
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
- }
-}
diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go
index 9122b97e..ea04a97a 100644
--- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go
+++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go
@@ -10,7 +10,6 @@ import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
- "encoding/json"
"fmt"
"math/big"
"strings"
@@ -137,9 +136,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
out, _ = sjson.Set(out, "stream", stream)
// Process messages and transform them to Claude Code format
- 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()
@@ -152,33 +148,23 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
role = "user"
}
- msg := map[string]interface{}{
- "role": role,
- "content": []interface{}{},
- }
+ msg := `{"role":"","content":[]}`
+ msg, _ = sjson.Set(msg, "role", role)
// Handle content based on its type (string or array)
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
- // Simple text content conversion
- msg["content"] = []interface{}{
- map[string]interface{}{
- "type": "text",
- "text": contentResult.String(),
- },
- }
+ part := `{"type":"text","text":""}`
+ part, _ = sjson.Set(part, "text", contentResult.String())
+ msg, _ = sjson.SetRaw(msg, "content.-1", part)
} else if contentResult.Exists() && contentResult.IsArray() {
- // 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(),
- })
+ textPart := `{"type":"text","text":""}`
+ textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
+ msg, _ = sjson.SetRaw(msg, "content.-1", textPart)
case "image_url":
// Convert OpenAI image format to Claude Code format
@@ -191,132 +177,95 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
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,
- },
- })
+ imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
+ imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
+ imagePart, _ = sjson.Set(imagePart, "source.data", data)
+ msg, _ = sjson.SetRaw(msg, "content.-1", imagePart)
}
}
}
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(),
- }
+ toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolUse, _ = sjson.Set(toolUse, "id", toolCallID)
+ toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String())
// Parse arguments for the tool call
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
+ if argsStr != "" && gjson.Valid(argsStr) {
+ argsJSON := gjson.Parse(argsStr)
+ if argsJSON.IsObject() {
+ toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
} else {
- toolUse["input"] = map[string]interface{}{}
+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
}
} else {
- toolUse["input"] = map[string]interface{}{}
+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
}
} else {
- toolUse["input"] = map[string]interface{}{}
+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
}
- contentParts = append(contentParts, toolUse)
+ msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
}
return true
})
- msg["content"] = contentParts
}
- anthropicMessages = append(anthropicMessages, msg)
+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
case "tool":
// Handle tool result messages conversion
toolCallID := message.Get("tool_call_id").String()
content := message.Get("content").String()
- // Create tool result message in Claude Code format
- msg := map[string]interface{}{
- "role": "user",
- "content": []interface{}{
- map[string]interface{}{
- "type": "tool_result",
- "tool_use_id": toolCallID,
- "content": content,
- },
- },
- }
-
- anthropicMessages = append(anthropicMessages, msg)
+ msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
+ msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
+ msg, _ = sjson.Set(msg, "content.0.content", content)
+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
}
return true
})
}
- // 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 -> Claude Code tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
- var anthropicTools []interface{}
+ hasAnthropicTools := false
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(),
- }
+ anthropicTool := `{"name":"","description":""}`
+ anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String())
+ anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String())
// 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()
+ anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
+ } else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() {
+ anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
}
- anthropicTools = append(anthropicTools, anthropicTool)
+ out, _ = sjson.SetRaw(out, "tools.-1", anthropicTool)
+ hasAnthropicTools = true
}
return true
})
- if len(anthropicTools) > 0 {
- toolsJSON, _ := json.Marshal(anthropicTools)
- out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
+ if !hasAnthropicTools {
+ out, _ = sjson.Delete(out, "tools")
}
}
@@ -329,18 +278,17 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
case "none":
// Don't set tool_choice, Claude Code will not use tools
case "auto":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
case "required":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
}
case gjson.JSON:
// 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{}{
- "type": "tool",
- "name": functionName,
- })
+ toolChoiceJSON := `{"type":"tool","name":""}`
+ toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName)
+ out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
}
default:
}
diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_response.go b/internal/translator/claude/openai/chat-completions/claude_openai_response.go
index f8fd4018..99b75749 100644
--- a/internal/translator/claude/openai/chat-completions/claude_openai_response.go
+++ b/internal/translator/claude/openai/chat-completions/claude_openai_response.go
@@ -8,7 +8,7 @@ package chat_completions
import (
"bytes"
"context"
- "encoding/json"
+ "fmt"
"strings"
"time"
@@ -178,18 +178,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
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})
+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index)
+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID)
+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function")
+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name)
+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.arguments", arguments)
// Clean up the accumulator for this index
delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
@@ -210,12 +203,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
// Handle usage information for token counts
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)
+ inputTokens := usage.Get("input_tokens").Int()
+ outputTokens := usage.Get("output_tokens").Int()
+ template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens)
+ template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens)
+ template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens)
}
return []string{template}
@@ -230,14 +222,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
case "error":
// Error event - format and return error response
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)}
+ errorJSON := `{"error":{"message":"","type":""}}`
+ errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String())
+ errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String())
+ return []string{errorJSON}
}
return []string{}
@@ -298,10 +286,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
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)
+ toolCallsAccumulator := make(map[int]*ToolCallAccumulator)
for _, chunk := range chunks {
root := gjson.ParseBytes(chunk)
@@ -327,18 +312,12 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
// Start of thinking/reasoning content - skip for now as it's handled in delta
continue
} else if blockType == "tool_use" {
- // Initialize tool call tracking for this index
+ // Initialize tool call accumulator for this index
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": "",
- },
+ toolCallsAccumulator[index] = &ToolCallAccumulator{
+ ID: contentBlock.Get("id").String(),
+ Name: contentBlock.Get("name").String(),
}
- // Initialize arguments builder for this tool call
- toolCallArgsMap[index] = strings.Builder{}
}
}
@@ -361,9 +340,8 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
// 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
+ if accumulator, exists := toolCallsAccumulator[index]; exists {
+ accumulator.Arguments.WriteString(partialJSON.String())
}
}
}
@@ -372,14 +350,9 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
case "content_block_stop":
// 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 for the tool call
- arguments := builder.String()
- if arguments == "" {
- arguments = "{}"
- }
- toolCall["function"].(map[string]interface{})["arguments"] = arguments
+ if accumulator, exists := toolCallsAccumulator[index]; exists {
+ if accumulator.Arguments.Len() == 0 {
+ accumulator.Arguments.WriteString("{}")
}
}
@@ -417,24 +390,35 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
}
// 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{}
- // Find the maximum index to determine the range
+ if len(toolCallsAccumulator) > 0 {
+ toolCallsCount := 0
maxIndex := -1
- for index := range toolCallsMap {
+ for index := range toolCallsAccumulator {
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)
+ accumulator, exists := toolCallsAccumulator[i]
+ if !exists {
+ continue
}
+
+ arguments := accumulator.Arguments.String()
+
+ idPath := fmt.Sprintf("choices.0.message.tool_calls.%d.id", toolCallsCount)
+ typePath := fmt.Sprintf("choices.0.message.tool_calls.%d.type", toolCallsCount)
+ namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount)
+ argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount)
+
+ out, _ = sjson.Set(out, idPath, accumulator.ID)
+ out, _ = sjson.Set(out, typePath, "function")
+ out, _ = sjson.Set(out, namePath, accumulator.Name)
+ out, _ = sjson.Set(out, argumentsPath, arguments)
+ toolCallsCount++
}
- if len(toolCallsArray) > 0 {
- out, _ = sjson.Set(out, "choices.0.message.tool_calls", toolCallsArray)
+ if toolCallsCount > 0 {
out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
} else {
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go
index b3654ca0..eae44205 100644
--- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go
+++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go
@@ -254,7 +254,10 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
toolUse, _ = sjson.Set(toolUse, "id", callID)
toolUse, _ = sjson.Set(toolUse, "name", name)
if argsStr != "" && gjson.Valid(argsStr) {
- toolUse, _ = sjson.SetRaw(toolUse, "input", argsStr)
+ argsJSON := gjson.Parse(argsStr)
+ if argsJSON.IsObject() {
+ toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
+ }
}
asst := `{"role":"assistant","content":[]}`
@@ -309,16 +312,18 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
case gjson.String:
switch toolChoice.String() {
case "auto":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
case "none":
// Leave unset; implies no tools
case "required":
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
}
case gjson.JSON:
if toolChoice.Get("type").String() == "function" {
fn := toolChoice.Get("function.name").String()
- out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "tool", "name": fn})
+ toolChoiceJSON := `{"name":"","type":"tool"}`
+ toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", fn)
+ out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
}
default:
diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_response.go b/internal/translator/claude/openai/responses/claude_openai-responses_response.go
index 8ce17f5c..354be56e 100644
--- a/internal/translator/claude/openai/responses/claude_openai-responses_response.go
+++ b/internal/translator/claude/openai/responses/claude_openai-responses_response.go
@@ -344,31 +344,20 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
}
// Build response.output from aggregated state
- var outputs []interface{}
+ outputsWrapper := `{"arr":[]}`
// reasoning item (if any)
if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
- r := map[string]interface{}{
- "id": st.ReasoningItemID,
- "type": "reasoning",
- "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},
- }
- outputs = append(outputs, r)
+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
+ item, _ = sjson.Set(item, "id", st.ReasoningItemID)
+ item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
// assistant message item (if any text)
if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {
- m := map[string]interface{}{
- "id": st.CurrentMsgID,
- "type": "message",
- "status": "completed",
- "content": []interface{}{map[string]interface{}{
- "type": "output_text",
- "annotations": []interface{}{},
- "logprobs": []interface{}{},
- "text": st.TextBuf.String(),
- }},
- "role": "assistant",
- }
- outputs = append(outputs, m)
+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
+ item, _ = sjson.Set(item, "id", st.CurrentMsgID)
+ item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
// function_call items (in ascending index order for determinism)
if len(st.FuncArgsBuf) > 0 {
@@ -395,19 +384,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
if callID == "" && st.CurrentFCID != "" {
callID = st.CurrentFCID
}
- item := map[string]interface{}{
- "id": fmt.Sprintf("fc_%s", callID),
- "type": "function_call",
- "status": "completed",
- "arguments": args,
- "call_id": callID,
- "name": name,
- }
- outputs = append(outputs, item)
+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
+ item, _ = sjson.Set(item, "arguments", args)
+ item, _ = sjson.Set(item, "call_id", callID)
+ item, _ = sjson.Set(item, "name", name)
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
}
- if len(outputs) > 0 {
- completed, _ = sjson.Set(completed, "response.output", outputs)
+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
+ completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
}
reasoningTokens := int64(0)
@@ -628,27 +614,18 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
}
// Build output array
- var outputs []interface{}
+ outputsWrapper := `{"arr":[]}`
if reasoningBuf.Len() > 0 {
- outputs = append(outputs, map[string]interface{}{
- "id": reasoningItemID,
- "type": "reasoning",
- "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": reasoningBuf.String()}},
- })
+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
+ item, _ = sjson.Set(item, "id", reasoningItemID)
+ item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
if currentMsgID != "" || textBuf.Len() > 0 {
- outputs = append(outputs, map[string]interface{}{
- "id": currentMsgID,
- "type": "message",
- "status": "completed",
- "content": []interface{}{map[string]interface{}{
- "type": "output_text",
- "annotations": []interface{}{},
- "logprobs": []interface{}{},
- "text": textBuf.String(),
- }},
- "role": "assistant",
- })
+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
+ item, _ = sjson.Set(item, "id", currentMsgID)
+ item, _ = sjson.Set(item, "content.0.text", textBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
if len(toolCalls) > 0 {
// Preserve index order
@@ -669,18 +646,16 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
if args == "" {
args = "{}"
}
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("fc_%s", st.id),
- "type": "function_call",
- "status": "completed",
- "arguments": args,
- "call_id": st.id,
- "name": st.name,
- })
+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id))
+ item, _ = sjson.Set(item, "arguments", args)
+ item, _ = sjson.Set(item, "call_id", st.id)
+ item, _ = sjson.Set(item, "name", st.name)
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
}
- if len(outputs) > 0 {
- out, _ = sjson.Set(out, "output", outputs)
+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
+ out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw)
}
// Usage
diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go
index e9fe758d..e3909d45 100644
--- a/internal/translator/codex/claude/codex_claude_response.go
+++ b/internal/translator/codex/claude/codex_claude_response.go
@@ -9,7 +9,6 @@ package claude
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
@@ -191,21 +190,12 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
return ""
}
- response := map[string]interface{}{
- "id": responseData.Get("id").String(),
- "type": "message",
- "role": "assistant",
- "model": responseData.Get("model").String(),
- "content": []interface{}{},
- "stop_reason": nil,
- "stop_sequence": nil,
- "usage": map[string]interface{}{
- "input_tokens": responseData.Get("usage.input_tokens").Int(),
- "output_tokens": responseData.Get("usage.output_tokens").Int(),
- },
- }
+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
+ out, _ = sjson.Set(out, "id", responseData.Get("id").String())
+ out, _ = sjson.Set(out, "model", responseData.Get("model").String())
+ out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
+ out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
- var contentBlocks []interface{}
hasToolCall := false
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
@@ -244,10 +234,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
}
}
if thinkingBuilder.Len() > 0 {
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "thinking",
- "thinking": thinkingBuilder.String(),
- })
+ block := `{"type":"thinking","thinking":""}`
+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
case "message":
if content := item.Get("content"); content.Exists() {
@@ -256,10 +245,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
if part.Get("type").String() == "output_text" {
text := part.Get("text").String()
if text != "" {
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "text",
- "text": text,
- })
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", text)
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
}
return true
@@ -267,10 +255,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
} else {
text := content.String()
if text != "" {
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "text",
- "text": text,
- })
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", text)
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
}
}
@@ -281,54 +268,41 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
name = original
}
- toolBlock := map[string]interface{}{
- "type": "tool_use",
- "id": item.Get("call_id").String(),
- "name": name,
- "input": map[string]interface{}{},
- }
-
- if argsStr := item.Get("arguments").String(); argsStr != "" {
- var args interface{}
- if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
- toolBlock["input"] = args
+ toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolBlock, _ = sjson.Set(toolBlock, "id", item.Get("call_id").String())
+ toolBlock, _ = sjson.Set(toolBlock, "name", name)
+ inputRaw := "{}"
+ if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
+ argsJSON := gjson.Parse(argsStr)
+ if argsJSON.IsObject() {
+ inputRaw = argsJSON.Raw
}
}
-
- contentBlocks = append(contentBlocks, toolBlock)
+ toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
+ out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
}
return true
})
}
- if len(contentBlocks) > 0 {
- response["content"] = contentBlocks
- }
-
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
- response["stop_reason"] = stopReason.String()
+ out, _ = sjson.Set(out, "stop_reason", stopReason.String())
} else if hasToolCall {
- response["stop_reason"] = "tool_use"
+ out, _ = sjson.Set(out, "stop_reason", "tool_use")
} else {
- response["stop_reason"] = "end_turn"
+ out, _ = sjson.Set(out, "stop_reason", "end_turn")
}
if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
- response["stop_sequence"] = stopSequence.Value()
+ out, _ = sjson.SetRaw(out, "stop_sequence", stopSequence.Raw)
}
if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
- response["usage"] = map[string]interface{}{
- "input_tokens": responseData.Get("usage.input_tokens").Int(),
- "output_tokens": responseData.Get("usage.output_tokens").Int(),
- }
+ out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
+ out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
}
- responseJSON, err := json.Marshal(response)
- if err != nil {
- return ""
- }
- return string(responseJSON)
+ return out
}
// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
diff --git a/internal/translator/codex/gemini/codex_gemini_response.go b/internal/translator/codex/gemini/codex_gemini_response.go
index 098e6228..82a2187f 100644
--- a/internal/translator/codex/gemini/codex_gemini_response.go
+++ b/internal/translator/codex/gemini/codex_gemini_response.go
@@ -7,7 +7,6 @@ package gemini
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"time"
@@ -190,19 +189,19 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
}
// Process output content to build parts array
- var parts []interface{}
hasToolCall := false
- var pendingFunctionCalls []interface{}
+ var pendingFunctionCalls []string
flushPendingFunctionCalls := func() {
- if len(pendingFunctionCalls) > 0 {
- // Add all pending function calls as individual parts
- // This maintains the original Gemini API format while ensuring consecutive calls are grouped together
- for _, fc := range pendingFunctionCalls {
- parts = append(parts, fc)
- }
- pendingFunctionCalls = nil
+ if len(pendingFunctionCalls) == 0 {
+ return
}
+ // Add all pending function calls as individual parts
+ // This maintains the original Gemini API format while ensuring consecutive calls are grouped together
+ for _, fc := range pendingFunctionCalls {
+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", fc)
+ }
+ pendingFunctionCalls = nil
}
if output := responseData.Get("output"); output.Exists() && output.IsArray() {
@@ -216,11 +215,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
// Add thinking content
if content := value.Get("content"); content.Exists() {
- part := map[string]interface{}{
- "thought": true,
- "text": content.String(),
- }
- parts = append(parts, part)
+ part := `{"text":"","thought":true}`
+ part, _ = sjson.Set(part, "text", content.String())
+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
}
case "message":
@@ -232,10 +229,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
content.ForEach(func(_, contentItem gjson.Result) bool {
if contentItem.Get("type").String() == "output_text" {
if text := contentItem.Get("text"); text.Exists() {
- part := map[string]interface{}{
- "text": text.String(),
- }
- parts = append(parts, part)
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", text.String())
+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
}
}
return true
@@ -245,28 +241,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
case "function_call":
// Collect function call for potential merging with consecutive ones
hasToolCall = true
- functionCall := map[string]interface{}{
- "functionCall": map[string]interface{}{
- "name": func() string {
- n := value.Get("name").String()
- rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
- if orig, ok := rev[n]; ok {
- return orig
- }
- return n
- }(),
- "args": map[string]interface{}{},
- },
+ functionCall := `{"functionCall":{"args":{},"name":""}}`
+ {
+ n := value.Get("name").String()
+ rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
+ if orig, ok := rev[n]; ok {
+ n = orig
+ }
+ functionCall, _ = sjson.Set(functionCall, "functionCall.name", n)
}
// Parse and set arguments
if argsStr := value.Get("arguments").String(); argsStr != "" {
argsResult := gjson.Parse(argsStr)
if argsResult.IsObject() {
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
- functionCall["functionCall"].(map[string]interface{})["args"] = args
- }
+ functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr)
}
}
@@ -279,11 +268,6 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
flushPendingFunctionCalls()
}
- // Set the parts array
- if len(parts) > 0 {
- template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
- }
-
// Set finish reason based on whether there were tool calls
if hasToolCall {
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
@@ -323,15 +307,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string {
return rev
}
-// mustMarshalJSON marshals a value to JSON, panicking on error.
-func mustMarshalJSON(v interface{}) string {
- data, err := json.Marshal(v)
- if err != nil {
- return ""
- }
- return string(data)
-}
-
func GeminiTokenCount(ctx context.Context, count int64) string {
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
}
diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go
index a74ced7c..66e0385f 100644
--- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go
+++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go
@@ -7,10 +7,8 @@ package claude
import (
"bytes"
- "encoding/json"
"strings"
- client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/tidwall/gjson"
@@ -41,92 +39,102 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
rawJSON := bytes.Clone(inputRawJSON)
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
+ // Build output Gemini CLI request JSON
+ out := `{"model":"","request":{"contents":[]}}`
+ out, _ = sjson.Set(out, "model", modelName)
+
// system instruction
- var systemInstruction *client.Content
- systemResult := gjson.GetBytes(rawJSON, "system")
- if systemResult.IsArray() {
- systemResults := systemResult.Array()
- systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
- for i := 0; i < len(systemResults); i++ {
- systemPromptResult := systemResults[i]
- systemTypePromptResult := systemPromptResult.Get("type")
- if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
- systemPrompt := systemPromptResult.Get("text").String()
- systemPart := client.Part{Text: systemPrompt}
- systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
+ if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
+ systemInstruction := `{"role":"user","parts":[]}`
+ hasSystemParts := false
+ systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
+ if systemPromptResult.Get("type").String() == "text" {
+ textResult := systemPromptResult.Get("text")
+ if textResult.Type == gjson.String {
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", textResult.String())
+ systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
+ hasSystemParts = true
+ }
}
+ return true
+ })
+ if hasSystemParts {
+ out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction)
}
- if len(systemInstruction.Parts) == 0 {
- systemInstruction = nil
- }
+ } else if systemResult.Type == gjson.String {
+ out, _ = sjson.Set(out, "request.systemInstruction.parts.-1.text", systemResult.String())
}
// contents
- contents := make([]client.Content, 0)
- messagesResult := gjson.GetBytes(rawJSON, "messages")
- if messagesResult.IsArray() {
- messageResults := messagesResult.Array()
- for i := 0; i < len(messageResults); i++ {
- messageResult := messageResults[i]
+ if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
+ messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
roleResult := messageResult.Get("role")
if roleResult.Type != gjson.String {
- continue
+ return true
}
role := roleResult.String()
if role == "assistant" {
role = "model"
}
- clientContent := client.Content{Role: role, Parts: []client.Part{}}
+
+ contentJSON := `{"role":"","parts":[]}`
+ contentJSON, _ = sjson.Set(contentJSON, "role", role)
+
contentsResult := messageResult.Get("content")
if contentsResult.IsArray() {
- contentResults := contentsResult.Array()
- for j := 0; j < len(contentResults); j++ {
- contentResult := contentResults[j]
- contentTypeResult := contentResult.Get("type")
- if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
- prompt := contentResult.Get("text").String()
- clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
- } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
+ contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
+ switch contentResult.Get("type").String() {
+ case "text":
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
+
+ case "tool_use":
functionName := contentResult.Get("name").String()
functionArgs := contentResult.Get("input").String()
- var args map[string]any
- if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
- clientContent.Parts = append(clientContent.Parts, client.Part{
- FunctionCall: &client.FunctionCall{Name: functionName, Args: args},
- ThoughtSignature: geminiCLIClaudeThoughtSignature,
- })
+ argsResult := gjson.Parse(functionArgs)
+ if argsResult.IsObject() && gjson.Valid(functionArgs) {
+ part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
+ part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature)
+ part, _ = sjson.Set(part, "functionCall.name", functionName)
+ part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
}
- } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
+
+ case "tool_result":
toolCallID := contentResult.Get("tool_use_id").String()
- if toolCallID != "" {
- funcName := toolCallID
- toolCallIDs := strings.Split(toolCallID, "-")
- if len(toolCallIDs) > 1 {
- funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
- }
- responseData := contentResult.Get("content").Raw
- functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
- clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
+ if toolCallID == "" {
+ return true
}
+ funcName := toolCallID
+ toolCallIDs := strings.Split(toolCallID, "-")
+ if len(toolCallIDs) > 1 {
+ funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
+ }
+ responseData := contentResult.Get("content").Raw
+ part := `{"functionResponse":{"name":"","response":{"result":""}}}`
+ part, _ = sjson.Set(part, "functionResponse.name", funcName)
+ part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
}
- }
- contents = append(contents, clientContent)
+ return true
+ })
+ out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
} else if contentsResult.Type == gjson.String {
- prompt := contentsResult.String()
- contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", contentsResult.String())
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
+ out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
}
- }
+ return true
+ })
}
// tools
- var tools []client.ToolDeclaration
- toolsResult := gjson.GetBytes(rawJSON, "tools")
- if toolsResult.IsArray() {
- tools = make([]client.ToolDeclaration, 1)
- tools[0].FunctionDeclarations = make([]any, 0)
- toolsResults := toolsResult.Array()
- for i := 0; i < len(toolsResults); i++ {
- toolResult := toolsResults[i]
+ if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
+ hasTools := false
+ toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
inputSchemaResult := toolResult.Get("input_schema")
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
inputSchema := inputSchemaResult.Raw
@@ -136,30 +144,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
tool, _ = sjson.Delete(tool, "input_examples")
tool, _ = sjson.Delete(tool, "type")
tool, _ = sjson.Delete(tool, "cache_control")
- var toolDeclaration any
- if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
- tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
+ if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
+ if !hasTools {
+ out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`)
+ hasTools = true
+ }
+ out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool)
}
}
+ return true
+ })
+ if !hasTools {
+ out, _ = sjson.Delete(out, "request.tools")
}
- } else {
- tools = make([]client.ToolDeclaration, 0)
- }
-
- // Build output Gemini CLI request JSON
- out := `{"model":"","request":{"contents":[]}}`
- out, _ = sjson.Set(out, "model", modelName)
- if systemInstruction != nil {
- b, _ := json.Marshal(systemInstruction)
- out, _ = sjson.SetRaw(out, "request.systemInstruction", string(b))
- }
- if len(contents) > 0 {
- b, _ := json.Marshal(contents)
- out, _ = sjson.SetRaw(out, "request.contents", string(b))
- }
- if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
- b, _ := json.Marshal(tools)
- out, _ = sjson.SetRaw(out, "request.tools", string(b))
}
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go
index ca905f9e..2f8e9548 100644
--- a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go
+++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go
@@ -9,7 +9,6 @@ package claude
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
"sync/atomic"
@@ -276,22 +275,16 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
root := gjson.ParseBytes(rawJSON)
- response := map[string]interface{}{
- "id": root.Get("response.responseId").String(),
- "type": "message",
- "role": "assistant",
- "model": root.Get("response.modelVersion").String(),
- "content": []interface{}{},
- "stop_reason": nil,
- "stop_sequence": nil,
- "usage": map[string]interface{}{
- "input_tokens": root.Get("response.usageMetadata.promptTokenCount").Int(),
- "output_tokens": root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int(),
- },
- }
+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
+ out, _ = sjson.Set(out, "id", root.Get("response.responseId").String())
+ out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String())
+
+ inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
+ outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int()
+ out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
+ out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
parts := root.Get("response.candidates.0.content.parts")
- var contentBlocks []interface{}
textBuilder := strings.Builder{}
thinkingBuilder := strings.Builder{}
toolIDCounter := 0
@@ -301,10 +294,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
if textBuilder.Len() == 0 {
return
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "text",
- "text": textBuilder.String(),
- })
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", textBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
textBuilder.Reset()
}
@@ -312,10 +304,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
if thinkingBuilder.Len() == 0 {
return
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "thinking",
- "thinking": thinkingBuilder.String(),
- })
+ block := `{"type":"thinking","thinking":""}`
+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
thinkingBuilder.Reset()
}
@@ -339,21 +330,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
name := functionCall.Get("name").String()
toolIDCounter++
- toolBlock := map[string]interface{}{
- "type": "tool_use",
- "id": fmt.Sprintf("tool_%d", toolIDCounter),
- "name": name,
- "input": map[string]interface{}{},
+ toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
+ toolBlock, _ = sjson.Set(toolBlock, "name", name)
+ inputRaw := "{}"
+ if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
+ inputRaw = args.Raw
}
-
- if args := functionCall.Get("args"); args.Exists() {
- var parsed interface{}
- if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil {
- toolBlock["input"] = parsed
- }
- }
-
- contentBlocks = append(contentBlocks, toolBlock)
+ toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
+ out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
continue
}
}
@@ -362,8 +347,6 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
flushThinking()
flushText()
- response["content"] = contentBlocks
-
stopReason := "end_turn"
if hasToolCall {
stopReason = "tool_use"
@@ -379,19 +362,13 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
}
}
}
- response["stop_reason"] = stopReason
+ out, _ = sjson.Set(out, "stop_reason", stopReason)
- if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {
- if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() {
- delete(response, "usage")
- }
+ if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() {
+ out, _ = sjson.Delete(out, "usage")
}
- encoded, err := json.Marshal(response)
- if err != nil {
- return ""
- }
- return string(encoded)
+ return out
}
func ClaudeTokenCount(ctx context.Context, count int64) string {
diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go
index 5008d584..ac6227fe 100644
--- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go
+++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go
@@ -7,7 +7,6 @@ package gemini
import (
"bytes"
- "encoding/json"
"fmt"
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
@@ -117,8 +116,6 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by
// FunctionCallGroup represents a group of function calls and their responses
type FunctionCallGroup struct {
- ModelContent map[string]interface{}
- FunctionCalls []gjson.Result
ResponsesNeeded int
}
@@ -146,7 +143,7 @@ func fixCLIToolResponse(input string) (string, error) {
}
// Initialize data structures for processing and grouping
- var newContents []interface{} // Final processed contents array
+ contentsWrapper := `{"contents":[]}`
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
var collectedResponses []gjson.Result // Standalone responses to be matched
@@ -178,23 +175,17 @@ func fixCLIToolResponse(input string) (string, error) {
collectedResponses = collectedResponses[group.ResponsesNeeded:]
// Create merged function response content
- var responseParts []interface{}
+ functionResponseContent := `{"parts":[],"role":"function"}`
for _, response := range groupResponses {
- var responseMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
+ if !response.IsObject() {
+ log.Warnf("failed to parse function response")
continue
}
- responseParts = append(responseParts, responseMap)
+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
}
- if len(responseParts) > 0 {
- functionResponseContent := map[string]interface{}{
- "parts": responseParts,
- "role": "function",
- }
- newContents = append(newContents, functionResponseContent)
+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
}
// Remove this group as it's been satisfied
@@ -208,50 +199,42 @@ func fixCLIToolResponse(input string) (string, error) {
// If this is a model with function calls, create a new group
if role == "model" {
- var functionCallsInThisModel []gjson.Result
+ functionCallsCount := 0
parts.ForEach(func(_, part gjson.Result) bool {
if part.Get("functionCall").Exists() {
- functionCallsInThisModel = append(functionCallsInThisModel, part)
+ functionCallsCount++
}
return true
})
- if len(functionCallsInThisModel) > 0 {
+ if functionCallsCount > 0 {
// Add the model content
- var contentMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
+ if !value.IsObject() {
+ log.Warnf("failed to parse model content")
return true
}
- newContents = append(newContents, contentMap)
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
// Create a new group for tracking responses
group := &FunctionCallGroup{
- ModelContent: contentMap,
- FunctionCalls: functionCallsInThisModel,
- ResponsesNeeded: len(functionCallsInThisModel),
+ ResponsesNeeded: functionCallsCount,
}
pendingGroups = append(pendingGroups, group)
} else {
// Regular model content without function calls
- var contentMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
+ if !value.IsObject() {
+ log.Warnf("failed to parse content")
return true
}
- newContents = append(newContents, contentMap)
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
}
} else {
// Non-model content (user, etc.)
- var contentMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
+ if !value.IsObject() {
+ log.Warnf("failed to parse content")
return true
}
- newContents = append(newContents, contentMap)
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
}
return true
@@ -263,31 +246,24 @@ func fixCLIToolResponse(input string) (string, error) {
groupResponses := collectedResponses[:group.ResponsesNeeded]
collectedResponses = collectedResponses[group.ResponsesNeeded:]
- var responseParts []interface{}
+ functionResponseContent := `{"parts":[],"role":"function"}`
for _, response := range groupResponses {
- var responseMap map[string]interface{}
- errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)
- if errUnmarshal != nil {
- log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
+ if !response.IsObject() {
+ log.Warnf("failed to parse function response")
continue
}
- responseParts = append(responseParts, responseMap)
+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
}
- if len(responseParts) > 0 {
- functionResponseContent := map[string]interface{}{
- "parts": responseParts,
- "role": "function",
- }
- newContents = append(newContents, functionResponseContent)
+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
}
}
}
// Update the original JSON with the new contents
result := input
- newContentsJSON, _ := json.Marshal(newContents)
- result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
+ result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
return result, nil
}
diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go
index 42365d18..7b0c5571 100644
--- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go
+++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go
@@ -160,6 +160,14 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
} else if content.IsObject() && content.Get("type").String() == "text" {
out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.0.text", content.Get("text").String())
+ } else if content.IsArray() {
+ contents := content.Array()
+ if len(contents) > 0 {
+ out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
+ for j := 0; j < len(contents); j++ {
+ out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", j), contents[j].Get("text").String())
+ }
+ }
}
} else if role == "user" || (role == "system" && len(arr) == 1) {
// Build single user content node to avoid splitting into multiple contents
@@ -278,7 +286,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
continue
}
- fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
if errSet != nil {
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
continue
@@ -293,7 +301,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
continue
}
- fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
if errSet != nil {
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
continue
diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go
index 753870f3..2b3ac37e 100644
--- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go
+++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go
@@ -8,7 +8,6 @@ package chat_completions
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
"sync/atomic"
@@ -171,21 +170,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
mimeType = "image/png"
}
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
- imagePayload, err := json.Marshal(map[string]any{
- "type": "image_url",
- "image_url": map[string]string{
- "url": imageURL,
- },
- })
- if err != nil {
- continue
- }
+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
imagesResult := gjson.Get(template, "choices.0.delta.images")
if !imagesResult.Exists() || !imagesResult.IsArray() {
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
}
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
- template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
}
}
}
diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go
index 40f4fac2..4ab6ab97 100644
--- a/internal/translator/gemini/claude/gemini_claude_request.go
+++ b/internal/translator/gemini/claude/gemini_claude_request.go
@@ -7,10 +7,8 @@ package claude
import (
"bytes"
- "encoding/json"
"strings"
- client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
"github.com/tidwall/gjson"
@@ -34,92 +32,102 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
rawJSON := bytes.Clone(inputRawJSON)
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
+ // Build output Gemini CLI request JSON
+ out := `{"contents":[]}`
+ out, _ = sjson.Set(out, "model", modelName)
+
// system instruction
- var systemInstruction *client.Content
- systemResult := gjson.GetBytes(rawJSON, "system")
- if systemResult.IsArray() {
- systemResults := systemResult.Array()
- systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
- for i := 0; i < len(systemResults); i++ {
- systemPromptResult := systemResults[i]
- systemTypePromptResult := systemPromptResult.Get("type")
- if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
- systemPrompt := systemPromptResult.Get("text").String()
- systemPart := client.Part{Text: systemPrompt}
- systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
+ if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
+ systemInstruction := `{"role":"user","parts":[]}`
+ hasSystemParts := false
+ systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
+ if systemPromptResult.Get("type").String() == "text" {
+ textResult := systemPromptResult.Get("text")
+ if textResult.Type == gjson.String {
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", textResult.String())
+ systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
+ hasSystemParts = true
+ }
}
+ return true
+ })
+ if hasSystemParts {
+ out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction)
}
- if len(systemInstruction.Parts) == 0 {
- systemInstruction = nil
- }
+ } else if systemResult.Type == gjson.String {
+ out, _ = sjson.Set(out, "request.system_instruction.parts.-1.text", systemResult.String())
}
// contents
- contents := make([]client.Content, 0)
- messagesResult := gjson.GetBytes(rawJSON, "messages")
- if messagesResult.IsArray() {
- messageResults := messagesResult.Array()
- for i := 0; i < len(messageResults); i++ {
- messageResult := messageResults[i]
+ if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
+ messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
roleResult := messageResult.Get("role")
if roleResult.Type != gjson.String {
- continue
+ return true
}
role := roleResult.String()
if role == "assistant" {
role = "model"
}
- clientContent := client.Content{Role: role, Parts: []client.Part{}}
+
+ contentJSON := `{"role":"","parts":[]}`
+ contentJSON, _ = sjson.Set(contentJSON, "role", role)
+
contentsResult := messageResult.Get("content")
if contentsResult.IsArray() {
- contentResults := contentsResult.Array()
- for j := 0; j < len(contentResults); j++ {
- contentResult := contentResults[j]
- contentTypeResult := contentResult.Get("type")
- if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
- prompt := contentResult.Get("text").String()
- clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
- } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
+ contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
+ switch contentResult.Get("type").String() {
+ case "text":
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
+
+ case "tool_use":
functionName := contentResult.Get("name").String()
functionArgs := contentResult.Get("input").String()
- var args map[string]any
- if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
- clientContent.Parts = append(clientContent.Parts, client.Part{
- FunctionCall: &client.FunctionCall{Name: functionName, Args: args},
- ThoughtSignature: geminiClaudeThoughtSignature,
- })
+ argsResult := gjson.Parse(functionArgs)
+ if argsResult.IsObject() && gjson.Valid(functionArgs) {
+ part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
+ part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature)
+ part, _ = sjson.Set(part, "functionCall.name", functionName)
+ part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
}
- } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
+
+ case "tool_result":
toolCallID := contentResult.Get("tool_use_id").String()
- if toolCallID != "" {
- funcName := toolCallID
- toolCallIDs := strings.Split(toolCallID, "-")
- if len(toolCallIDs) > 1 {
- funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
- }
- responseData := contentResult.Get("content").Raw
- functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
- clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
+ if toolCallID == "" {
+ return true
}
+ funcName := toolCallID
+ toolCallIDs := strings.Split(toolCallID, "-")
+ if len(toolCallIDs) > 1 {
+ funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
+ }
+ responseData := contentResult.Get("content").Raw
+ part := `{"functionResponse":{"name":"","response":{"result":""}}}`
+ part, _ = sjson.Set(part, "functionResponse.name", funcName)
+ part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
}
- }
- contents = append(contents, clientContent)
+ return true
+ })
+ out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
} else if contentsResult.Type == gjson.String {
- prompt := contentsResult.String()
- contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
+ part := `{"text":""}`
+ part, _ = sjson.Set(part, "text", contentsResult.String())
+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
+ out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
}
- }
+ return true
+ })
}
// tools
- var tools []client.ToolDeclaration
- toolsResult := gjson.GetBytes(rawJSON, "tools")
- if toolsResult.IsArray() {
- tools = make([]client.ToolDeclaration, 1)
- tools[0].FunctionDeclarations = make([]any, 0)
- toolsResults := toolsResult.Array()
- for i := 0; i < len(toolsResults); i++ {
- toolResult := toolsResults[i]
+ if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
+ hasTools := false
+ toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
inputSchemaResult := toolResult.Get("input_schema")
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
inputSchema := inputSchemaResult.Raw
@@ -129,30 +137,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
tool, _ = sjson.Delete(tool, "input_examples")
tool, _ = sjson.Delete(tool, "type")
tool, _ = sjson.Delete(tool, "cache_control")
- var toolDeclaration any
- if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
- tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
+ if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
+ if !hasTools {
+ out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`)
+ hasTools = true
+ }
+ out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool)
}
}
+ return true
+ })
+ if !hasTools {
+ out, _ = sjson.Delete(out, "tools")
}
- } else {
- tools = make([]client.ToolDeclaration, 0)
- }
-
- // Build output Gemini CLI request JSON
- out := `{"contents":[]}`
- out, _ = sjson.Set(out, "model", modelName)
- if systemInstruction != nil {
- b, _ := json.Marshal(systemInstruction)
- out, _ = sjson.SetRaw(out, "system_instruction", string(b))
- }
- if len(contents) > 0 {
- b, _ := json.Marshal(contents)
- out, _ = sjson.SetRaw(out, "contents", string(b))
- }
- if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
- b, _ := json.Marshal(tools)
- out, _ = sjson.SetRaw(out, "tools", string(b))
}
// Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
diff --git a/internal/translator/gemini/claude/gemini_claude_response.go b/internal/translator/gemini/claude/gemini_claude_response.go
index d56b78e4..db14c78a 100644
--- a/internal/translator/gemini/claude/gemini_claude_response.go
+++ b/internal/translator/gemini/claude/gemini_claude_response.go
@@ -9,7 +9,6 @@ package claude
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
"sync/atomic"
@@ -282,22 +281,16 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
root := gjson.ParseBytes(rawJSON)
- response := map[string]interface{}{
- "id": root.Get("responseId").String(),
- "type": "message",
- "role": "assistant",
- "model": root.Get("modelVersion").String(),
- "content": []interface{}{},
- "stop_reason": nil,
- "stop_sequence": nil,
- "usage": map[string]interface{}{
- "input_tokens": root.Get("usageMetadata.promptTokenCount").Int(),
- "output_tokens": root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int(),
- },
- }
+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
+ out, _ = sjson.Set(out, "id", root.Get("responseId").String())
+ out, _ = sjson.Set(out, "model", root.Get("modelVersion").String())
+
+ inputTokens := root.Get("usageMetadata.promptTokenCount").Int()
+ outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int()
+ out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
+ out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
parts := root.Get("candidates.0.content.parts")
- var contentBlocks []interface{}
textBuilder := strings.Builder{}
thinkingBuilder := strings.Builder{}
toolIDCounter := 0
@@ -307,10 +300,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
if textBuilder.Len() == 0 {
return
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "text",
- "text": textBuilder.String(),
- })
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", textBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
textBuilder.Reset()
}
@@ -318,10 +310,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
if thinkingBuilder.Len() == 0 {
return
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "thinking",
- "thinking": thinkingBuilder.String(),
- })
+ block := `{"type":"thinking","thinking":""}`
+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
thinkingBuilder.Reset()
}
@@ -345,21 +336,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
name := functionCall.Get("name").String()
toolIDCounter++
- toolBlock := map[string]interface{}{
- "type": "tool_use",
- "id": fmt.Sprintf("tool_%d", toolIDCounter),
- "name": name,
- "input": map[string]interface{}{},
+ toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
+ toolBlock, _ = sjson.Set(toolBlock, "name", name)
+ inputRaw := "{}"
+ if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
+ inputRaw = args.Raw
}
-
- if args := functionCall.Get("args"); args.Exists() {
- var parsed interface{}
- if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil {
- toolBlock["input"] = parsed
- }
- }
-
- contentBlocks = append(contentBlocks, toolBlock)
+ toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
+ out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
continue
}
}
@@ -368,8 +353,6 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
flushThinking()
flushText()
- response["content"] = contentBlocks
-
stopReason := "end_turn"
if hasToolCall {
stopReason = "tool_use"
@@ -385,19 +368,13 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
}
}
}
- response["stop_reason"] = stopReason
+ out, _ = sjson.Set(out, "stop_reason", stopReason)
- if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {
- if usageMeta := root.Get("usageMetadata"); !usageMeta.Exists() {
- delete(response, "usage")
- }
+ if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() {
+ out, _ = sjson.Delete(out, "usage")
}
- encoded, err := json.Marshal(response)
- if err != nil {
- return ""
- }
- return string(encoded)
+ return out
}
func ClaudeTokenCount(ctx context.Context, count int64) string {
diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go
index c5f26fbd..195b0ae6 100644
--- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go
+++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go
@@ -178,6 +178,14 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
} else if content.IsObject() && content.Get("type").String() == "text" {
out, _ = sjson.SetBytes(out, "system_instruction.role", "user")
out, _ = sjson.SetBytes(out, "system_instruction.parts.0.text", content.Get("text").String())
+ } else if content.IsArray() {
+ contents := content.Array()
+ if len(contents) > 0 {
+ out, _ = sjson.SetBytes(out, "request.systemInstruction.role", "user")
+ for j := 0; j < len(contents); j++ {
+ out, _ = sjson.SetBytes(out, fmt.Sprintf("request.systemInstruction.parts.%d.text", j), contents[j].Get("text").String())
+ }
+ }
}
} else if role == "user" || (role == "system" && len(arr) == 1) {
// Build single user content node to avoid splitting into multiple contents
@@ -320,7 +328,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
continue
}
- fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
if errSet != nil {
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
continue
@@ -335,7 +343,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
continue
}
- fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
if errSet != nil {
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
continue
diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go
index b2a44e9e..d710b1d6 100644
--- a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go
+++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go
@@ -8,7 +8,6 @@ package chat_completions
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
"sync/atomic"
@@ -183,21 +182,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
mimeType = "image/png"
}
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
- imagePayload, err := json.Marshal(map[string]any{
- "type": "image_url",
- "image_url": map[string]string{
- "url": imageURL,
- },
- })
- if err != nil {
- continue
- }
+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
imagesResult := gjson.Get(template, "choices.0.delta.images")
if !imagesResult.Exists() || !imagesResult.IsArray() {
template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
}
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
- template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
}
}
}
@@ -324,21 +316,14 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
mimeType = "image/png"
}
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
- imagePayload, err := json.Marshal(map[string]any{
- "type": "image_url",
- "image_url": map[string]string{
- "url": imageURL,
- },
- })
- if err != nil {
- continue
- }
+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
imagesResult := gjson.Get(template, "choices.0.message.images")
if !imagesResult.Exists() || !imagesResult.IsArray() {
template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)
}
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
- template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", string(imagePayload))
+ template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", imagePayload)
}
}
}
diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go
index 1e2874c4..27d2f9b6 100644
--- a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go
+++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go
@@ -377,27 +377,18 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
}
// Compose outputs in encountered order: reasoning, message, function_calls
- var outputs []interface{}
+ outputsWrapper := `{"arr":[]}`
if st.ReasoningOpened {
- outputs = append(outputs, map[string]interface{}{
- "id": st.ReasoningItemID,
- "type": "reasoning",
- "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},
- })
+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
+ item, _ = sjson.Set(item, "id", st.ReasoningItemID)
+ item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
if st.MsgOpened {
- outputs = append(outputs, map[string]interface{}{
- "id": st.CurrentMsgID,
- "type": "message",
- "status": "completed",
- "content": []interface{}{map[string]interface{}{
- "type": "output_text",
- "annotations": []interface{}{},
- "logprobs": []interface{}{},
- "text": st.TextBuf.String(),
- }},
- "role": "assistant",
- })
+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
+ item, _ = sjson.Set(item, "id", st.CurrentMsgID)
+ item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
if len(st.FuncArgsBuf) > 0 {
idxs := make([]int, 0, len(st.FuncArgsBuf))
@@ -416,18 +407,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
if b := st.FuncArgsBuf[idx]; b != nil {
args = b.String()
}
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]),
- "type": "function_call",
- "status": "completed",
- "arguments": args,
- "call_id": st.FuncCallIDs[idx],
- "name": st.FuncNames[idx],
- })
+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
+ item, _ = sjson.Set(item, "arguments", args)
+ item, _ = sjson.Set(item, "call_id", st.FuncCallIDs[idx])
+ item, _ = sjson.Set(item, "name", st.FuncNames[idx])
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
}
- if len(outputs) > 0 {
- completed, _ = sjson.Set(completed, "response.output", outputs)
+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
+ completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
}
// usage mapping
@@ -558,11 +547,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
}
// Build outputs from candidates[0].content.parts
- var outputs []interface{}
var reasoningText strings.Builder
var reasoningEncrypted string
var messageText strings.Builder
var haveMessage bool
+
+ haveOutput := false
+ ensureOutput := func() {
+ if haveOutput {
+ return
+ }
+ resp, _ = sjson.SetRaw(resp, "output", "[]")
+ haveOutput = true
+ }
+ appendOutput := func(itemJSON string) {
+ ensureOutput()
+ resp, _ = sjson.SetRaw(resp, "output.-1", itemJSON)
+ }
+
if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() {
parts.ForEach(func(_, p gjson.Result) bool {
if p.Get("thought").Bool() {
@@ -583,19 +585,16 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
name := fc.Get("name").String()
args := fc.Get("args")
callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("fc_%s", callID),
- "type": "function_call",
- "status": "completed",
- "arguments": func() string {
- if args.Exists() {
- return args.Raw
- }
- return ""
- }(),
- "call_id": callID,
- "name": name,
- })
+ itemJSON := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
+ itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID))
+ itemJSON, _ = sjson.Set(itemJSON, "call_id", callID)
+ itemJSON, _ = sjson.Set(itemJSON, "name", name)
+ argsStr := ""
+ if args.Exists() {
+ argsStr = args.Raw
+ }
+ itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr)
+ appendOutput(itemJSON)
return true
}
return true
@@ -605,42 +604,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
// Reasoning output item
if reasoningText.Len() > 0 || reasoningEncrypted != "" {
rid := strings.TrimPrefix(id, "resp_")
- item := map[string]interface{}{
- "id": fmt.Sprintf("rs_%s", rid),
- "type": "reasoning",
- "encrypted_content": reasoningEncrypted,
- }
- var summaries []interface{}
+ itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}`
+ itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid))
+ itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted)
if reasoningText.Len() > 0 {
- summaries = append(summaries, map[string]interface{}{
- "type": "summary_text",
- "text": reasoningText.String(),
- })
+ summaryJSON := `{"type":"summary_text","text":""}`
+ summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String())
+ itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]")
+ itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON)
}
- if summaries != nil {
- item["summary"] = summaries
- }
- outputs = append(outputs, item)
+ appendOutput(itemJSON)
}
// Assistant message output item
if haveMessage {
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")),
- "type": "message",
- "status": "completed",
- "content": []interface{}{map[string]interface{}{
- "type": "output_text",
- "annotations": []interface{}{},
- "logprobs": []interface{}{},
- "text": messageText.String(),
- }},
- "role": "assistant",
- })
- }
-
- if len(outputs) > 0 {
- resp, _ = sjson.Set(resp, "output", outputs)
+ itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
+ itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")))
+ itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String())
+ appendOutput(itemJSON)
}
// usage mapping
diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go
index e61ec521..b6fd1e09 100644
--- a/internal/translator/openai/claude/openai_claude_request.go
+++ b/internal/translator/openai/claude/openai_claude_request.go
@@ -7,7 +7,6 @@ package claude
import (
"bytes"
- "encoding/json"
"strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
@@ -138,11 +137,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
// Convert input to arguments JSON string
if input := part.Get("input"); input.Exists() {
- if inputJSON, err := json.Marshal(input.Value()); err == nil {
- toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", string(inputJSON))
- } else {
- toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
- }
+ toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw)
} else {
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
}
@@ -191,8 +186,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
// Emit tool calls in a separate assistant message
if role == "assistant" && len(toolCalls) > 0 {
toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`
- toolCallsJSON, _ := json.Marshal(toolCalls)
- toolCallMsgJSON, _ = sjson.SetRaw(toolCallMsgJSON, "tool_calls", string(toolCallsJSON))
+ toolCallMsgJSON, _ = sjson.Set(toolCallMsgJSON, "tool_calls", toolCalls)
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())
}
diff --git a/internal/translator/openai/claude/openai_claude_response.go b/internal/translator/openai/claude/openai_claude_response.go
index af790dca..3c30299f 100644
--- a/internal/translator/openai/claude/openai_claude_response.go
+++ b/internal/translator/openai/claude/openai_claude_response.go
@@ -8,7 +8,6 @@ package claude
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strings"
@@ -133,24 +132,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
if delta := root.Get("choices.0.delta"); delta.Exists() {
if !param.MessageStarted {
// Send message_start event
- messageStart := map[string]interface{}{
- "type": "message_start",
- "message": map[string]interface{}{
- "id": param.MessageID,
- "type": "message",
- "role": "assistant",
- "model": param.Model,
- "content": []interface{}{},
- "stop_reason": nil,
- "stop_sequence": nil,
- "usage": map[string]interface{}{
- "input_tokens": 0,
- "output_tokens": 0,
- },
- },
- }
- messageStartJSON, _ := json.Marshal(messageStart)
- results = append(results, "event: message_start\ndata: "+string(messageStartJSON)+"\n\n")
+ messageStartJSON := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`
+ messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID)
+ messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model)
+ results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n")
param.MessageStarted = true
// Don't send content_block_start for text here - wait for actual content
@@ -168,29 +153,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
param.ThinkingContentBlockIndex = param.NextContentBlockIndex
param.NextContentBlockIndex++
}
- contentBlockStart := map[string]interface{}{
- "type": "content_block_start",
- "index": param.ThinkingContentBlockIndex,
- "content_block": map[string]interface{}{
- "type": "thinking",
- "thinking": "",
- },
- }
- contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
- results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
+ contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex)
+ results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
param.ThinkingContentBlockStarted = true
}
- thinkingDelta := map[string]interface{}{
- "type": "content_block_delta",
- "index": param.ThinkingContentBlockIndex,
- "delta": map[string]interface{}{
- "type": "thinking_delta",
- "thinking": reasoningText,
- },
- }
- thinkingDeltaJSON, _ := json.Marshal(thinkingDelta)
- results = append(results, "event: content_block_delta\ndata: "+string(thinkingDeltaJSON)+"\n\n")
+ thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
+ thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex)
+ thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText)
+ results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n")
}
}
@@ -203,29 +175,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
param.TextContentBlockIndex = param.NextContentBlockIndex
param.NextContentBlockIndex++
}
- contentBlockStart := map[string]interface{}{
- "type": "content_block_start",
- "index": param.TextContentBlockIndex,
- "content_block": map[string]interface{}{
- "type": "text",
- "text": "",
- },
- }
- contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
- results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
+ contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex)
+ results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
param.TextContentBlockStarted = true
}
- contentDelta := map[string]interface{}{
- "type": "content_block_delta",
- "index": param.TextContentBlockIndex,
- "delta": map[string]interface{}{
- "type": "text_delta",
- "text": content.String(),
- },
- }
- contentDeltaJSON, _ := json.Marshal(contentDelta)
- results = append(results, "event: content_block_delta\ndata: "+string(contentDeltaJSON)+"\n\n")
+ contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
+ contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex)
+ contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String())
+ results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n")
// Accumulate content
param.ContentAccumulator.WriteString(content.String())
@@ -263,18 +222,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
stopTextContentBlock(param, &results)
// Send content_block_start for tool_use
- contentBlockStart := map[string]interface{}{
- "type": "content_block_start",
- "index": blockIndex,
- "content_block": map[string]interface{}{
- "type": "tool_use",
- "id": accumulator.ID,
- "name": accumulator.Name,
- "input": map[string]interface{}{},
- },
- }
- contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
- results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
+ contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", accumulator.ID)
+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
+ results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
}
// Handle function arguments
@@ -298,12 +250,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
// Send content_block_stop for thinking content if needed
if param.ThinkingContentBlockStarted {
- contentBlockStop := map[string]interface{}{
- "type": "content_block_stop",
- "index": param.ThinkingContentBlockIndex,
- }
- contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
- results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
param.ThinkingContentBlockStarted = false
param.ThinkingContentBlockIndex = -1
}
@@ -319,24 +268,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
// Send complete input_json_delta with all accumulated arguments
if accumulator.Arguments.Len() > 0 {
- inputDelta := map[string]interface{}{
- "type": "content_block_delta",
- "index": blockIndex,
- "delta": map[string]interface{}{
- "type": "input_json_delta",
- "partial_json": util.FixJSON(accumulator.Arguments.String()),
- },
- }
- inputDeltaJSON, _ := json.Marshal(inputDelta)
- results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
+ inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
+ results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
}
- contentBlockStop := map[string]interface{}{
- "type": "content_block_stop",
- "index": blockIndex,
- }
- contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
- results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
delete(param.ToolCallBlockIndexes, index)
}
param.ContentBlocksStopped = true
@@ -361,20 +301,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
}
}
// Send message_delta with usage
- messageDelta := map[string]interface{}{
- "type": "message_delta",
- "delta": map[string]interface{}{
- "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
- "stop_sequence": nil,
- },
- "usage": map[string]interface{}{
- "input_tokens": inputTokens,
- "output_tokens": outputTokens,
- },
- }
-
- messageDeltaJSON, _ := json.Marshal(messageDelta)
- results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
+ messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
+ results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
param.MessageDeltaSent = true
emitMessageStopIfNeeded(param, &results)
@@ -390,12 +321,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
// Ensure all content blocks are stopped before final events
if param.ThinkingContentBlockStarted {
- contentBlockStop := map[string]interface{}{
- "type": "content_block_stop",
- "index": param.ThinkingContentBlockIndex,
- }
- contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
- results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
param.ThinkingContentBlockStarted = false
param.ThinkingContentBlockIndex = -1
}
@@ -408,24 +336,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
blockIndex := param.toolContentBlockIndex(index)
if accumulator.Arguments.Len() > 0 {
- inputDelta := map[string]interface{}{
- "type": "content_block_delta",
- "index": blockIndex,
- "delta": map[string]interface{}{
- "type": "input_json_delta",
- "partial_json": util.FixJSON(accumulator.Arguments.String()),
- },
- }
- inputDeltaJSON, _ := json.Marshal(inputDelta)
- results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
+ inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
+ results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
}
- contentBlockStop := map[string]interface{}{
- "type": "content_block_stop",
- "index": blockIndex,
- }
- contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
- results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
delete(param.ToolCallBlockIndexes, index)
}
param.ContentBlocksStopped = true
@@ -433,16 +352,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
// If we haven't sent message_delta yet (no usage info was received), send it now
if param.FinishReason != "" && !param.MessageDeltaSent {
- messageDelta := map[string]interface{}{
- "type": "message_delta",
- "delta": map[string]interface{}{
- "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
- "stop_sequence": nil,
- },
- }
-
- messageDeltaJSON, _ := json.Marshal(messageDelta)
- results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
+ messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null}}`
+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
+ results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
param.MessageDeltaSent = true
}
@@ -455,105 +367,73 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
root := gjson.ParseBytes(rawJSON)
- // Build Anthropic response
- response := map[string]interface{}{
- "id": root.Get("id").String(),
- "type": "message",
- "role": "assistant",
- "model": root.Get("model").String(),
- "content": []interface{}{},
- "stop_reason": nil,
- "stop_sequence": nil,
- "usage": map[string]interface{}{
- "input_tokens": 0,
- "output_tokens": 0,
- },
- }
+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
+ out, _ = sjson.Set(out, "id", root.Get("id").String())
+ out, _ = sjson.Set(out, "model", root.Get("model").String())
// Process message content and tool calls
- var contentBlocks []interface{}
-
- if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
+ if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
choice := choices.Array()[0] // Take first choice
- reasoningNode := choice.Get("message.reasoning_content")
- allReasoning := collectOpenAIReasoningTexts(reasoningNode)
- for _, reasoningText := range allReasoning {
+ reasoningNode := choice.Get("message.reasoning_content")
+ for _, reasoningText := range collectOpenAIReasoningTexts(reasoningNode) {
if reasoningText == "" {
continue
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "thinking",
- "thinking": reasoningText,
- })
+ block := `{"type":"thinking","thinking":""}`
+ block, _ = sjson.Set(block, "thinking", reasoningText)
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
// Handle text content
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
- textBlock := map[string]interface{}{
- "type": "text",
- "text": content.String(),
- }
- contentBlocks = append(contentBlocks, textBlock)
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", content.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
// Handle tool calls
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
- toolUseBlock := map[string]interface{}{
- "type": "tool_use",
- "id": toolCall.Get("id").String(),
- "name": toolCall.Get("function.name").String(),
- }
+ toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
+ toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
- // Parse arguments
- argsStr := toolCall.Get("function.arguments").String()
- argsStr = util.FixJSON(argsStr)
- if argsStr != "" {
- var args interface{}
- if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
- toolUseBlock["input"] = args
+ argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
+ if argsStr != "" && gjson.Valid(argsStr) {
+ argsJSON := gjson.Parse(argsStr)
+ if argsJSON.IsObject() {
+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
} else {
- toolUseBlock["input"] = map[string]interface{}{}
+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
}
} else {
- toolUseBlock["input"] = map[string]interface{}{}
+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
}
- contentBlocks = append(contentBlocks, toolUseBlock)
+ out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
return true
})
}
// Set stop reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
- response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
+ out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
}
}
- response["content"] = contentBlocks
-
// Set usage information
if usage := root.Get("usage"); usage.Exists() {
- response["usage"] = map[string]interface{}{
- "input_tokens": usage.Get("prompt_tokens").Int(),
- "output_tokens": usage.Get("completion_tokens").Int(),
- "reasoning_tokens": func() int64 {
- if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
- return v.Int()
- }
- return 0
- }(),
- }
- } else {
- response["usage"] = map[string]interface{}{
- "input_tokens": 0,
- "output_tokens": 0,
+ out, _ = sjson.Set(out, "usage.input_tokens", usage.Get("prompt_tokens").Int())
+ out, _ = sjson.Set(out, "usage.output_tokens", usage.Get("completion_tokens").Int())
+ reasoningTokens := int64(0)
+ if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
+ reasoningTokens = v.Int()
}
+ out, _ = sjson.Set(out, "usage.reasoning_tokens", reasoningTokens)
}
- responseJSON, _ := json.Marshal(response)
- return []string{string(responseJSON)}
+ return []string{out}
}
// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
@@ -620,12 +500,9 @@ func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, res
if !param.ThinkingContentBlockStarted {
return
}
- contentBlockStop := map[string]interface{}{
- "type": "content_block_stop",
- "index": param.ThinkingContentBlockIndex,
- }
- contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
- *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
+ *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
param.ThinkingContentBlockStarted = false
param.ThinkingContentBlockIndex = -1
}
@@ -642,12 +519,9 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results
if !param.TextContentBlockStarted {
return
}
- contentBlockStop := map[string]interface{}{
- "type": "content_block_stop",
- "index": param.TextContentBlockIndex,
- }
- contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
- *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex)
+ *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
param.TextContentBlockStarted = false
param.TextContentBlockIndex = -1
}
@@ -667,29 +541,19 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
_ = requestRawJSON
root := gjson.ParseBytes(rawJSON)
+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
+ out, _ = sjson.Set(out, "id", root.Get("id").String())
+ out, _ = sjson.Set(out, "model", root.Get("model").String())
- response := map[string]interface{}{
- "id": root.Get("id").String(),
- "type": "message",
- "role": "assistant",
- "model": root.Get("model").String(),
- "content": []interface{}{},
- "stop_reason": nil,
- "stop_sequence": nil,
- "usage": map[string]interface{}{
- "input_tokens": 0,
- "output_tokens": 0,
- },
- }
-
- contentBlocks := make([]interface{}, 0)
hasToolCall := false
+ stopReasonSet := false
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
choice := choices.Array()[0]
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
- response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
+ out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
+ stopReasonSet = true
}
if message := choice.Get("message"); message.Exists() {
@@ -702,10 +566,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
if textBuilder.Len() == 0 {
return
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "text",
- "text": textBuilder.String(),
- })
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", textBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
textBuilder.Reset()
}
@@ -713,16 +576,14 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
if thinkingBuilder.Len() == 0 {
return
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "thinking",
- "thinking": thinkingBuilder.String(),
- })
+ block := `{"type":"thinking","thinking":""}`
+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
+ out, _ = sjson.SetRaw(out, "content.-1", block)
thinkingBuilder.Reset()
}
for _, item := range contentResult.Array() {
- typeStr := item.Get("type").String()
- switch typeStr {
+ switch item.Get("type").String() {
case "text":
flushThinking()
textBuilder.WriteString(item.Get("text").String())
@@ -733,25 +594,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
if toolCalls.IsArray() {
toolCalls.ForEach(func(_, tc gjson.Result) bool {
hasToolCall = true
- toolUse := map[string]interface{}{
- "type": "tool_use",
- "id": tc.Get("id").String(),
- "name": tc.Get("function.name").String(),
- }
+ toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String())
+ toolUse, _ = sjson.Set(toolUse, "name", tc.Get("function.name").String())
argsStr := util.FixJSON(tc.Get("function.arguments").String())
- if argsStr != "" {
- var parsed interface{}
- if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil {
- toolUse["input"] = parsed
+ if argsStr != "" && gjson.Valid(argsStr) {
+ argsJSON := gjson.Parse(argsStr)
+ if argsJSON.IsObject() {
+ toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
} else {
- toolUse["input"] = map[string]interface{}{}
+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
}
} else {
- toolUse["input"] = map[string]interface{}{}
+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
}
- contentBlocks = append(contentBlocks, toolUse)
+ out, _ = sjson.SetRaw(out, "content.-1", toolUse)
return true
})
}
@@ -771,10 +630,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
} else if contentResult.Type == gjson.String {
textContent := contentResult.String()
if textContent != "" {
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "text",
- "text": textContent,
- })
+ block := `{"type":"text","text":""}`
+ block, _ = sjson.Set(block, "text", textContent)
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
}
}
@@ -784,81 +642,52 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
if reasoningText == "" {
continue
}
- contentBlocks = append(contentBlocks, map[string]interface{}{
- "type": "thinking",
- "thinking": reasoningText,
- })
+ block := `{"type":"thinking","thinking":""}`
+ block, _ = sjson.Set(block, "thinking", reasoningText)
+ out, _ = sjson.SetRaw(out, "content.-1", block)
}
}
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
hasToolCall = true
- toolUseBlock := map[string]interface{}{
- "type": "tool_use",
- "id": toolCall.Get("id").String(),
- "name": toolCall.Get("function.name").String(),
- }
+ toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
+ toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
+ toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
- argsStr := toolCall.Get("function.arguments").String()
- argsStr = util.FixJSON(argsStr)
- if argsStr != "" {
- var args interface{}
- if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
- toolUseBlock["input"] = args
+ argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
+ if argsStr != "" && gjson.Valid(argsStr) {
+ argsJSON := gjson.Parse(argsStr)
+ if argsJSON.IsObject() {
+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
} else {
- toolUseBlock["input"] = map[string]interface{}{}
+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
}
} else {
- toolUseBlock["input"] = map[string]interface{}{}
+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
}
- contentBlocks = append(contentBlocks, toolUseBlock)
+ out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
return true
})
}
}
}
- response["content"] = contentBlocks
-
if respUsage := root.Get("usage"); respUsage.Exists() {
- usageJSON := `{}`
- usageJSON, _ = sjson.Set(usageJSON, "input_tokens", respUsage.Get("prompt_tokens").Int())
- usageJSON, _ = sjson.Set(usageJSON, "output_tokens", respUsage.Get("completion_tokens").Int())
- parsedUsage := gjson.Parse(usageJSON).Value().(map[string]interface{})
- response["usage"] = parsedUsage
- } else {
- response["usage"] = `{"input_tokens":0,"output_tokens":0}`
+ out, _ = sjson.Set(out, "usage.input_tokens", respUsage.Get("prompt_tokens").Int())
+ out, _ = sjson.Set(out, "usage.output_tokens", respUsage.Get("completion_tokens").Int())
}
- if response["stop_reason"] == nil {
+ if !stopReasonSet {
if hasToolCall {
- response["stop_reason"] = "tool_use"
+ out, _ = sjson.Set(out, "stop_reason", "tool_use")
} else {
- response["stop_reason"] = "end_turn"
+ out, _ = sjson.Set(out, "stop_reason", "end_turn")
}
}
- if !hasToolCall {
- if toolBlocks := response["content"].([]interface{}); len(toolBlocks) > 0 {
- for _, block := range toolBlocks {
- if m, ok := block.(map[string]interface{}); ok && m["type"] == "tool_use" {
- hasToolCall = true
- break
- }
- }
- }
- if hasToolCall {
- response["stop_reason"] = "tool_use"
- }
- }
-
- responseJSON, err := json.Marshal(response)
- if err != nil {
- return ""
- }
- return string(responseJSON)
+ return out
}
func ClaudeTokenCount(ctx context.Context, count int64) string {
diff --git a/internal/translator/openai/gemini/openai_gemini_request.go b/internal/translator/openai/gemini/openai_gemini_request.go
index 032ca60d..f51d914b 100644
--- a/internal/translator/openai/gemini/openai_gemini_request.go
+++ b/internal/translator/openai/gemini/openai_gemini_request.go
@@ -8,7 +8,6 @@ package gemini
import (
"bytes"
"crypto/rand"
- "encoding/json"
"fmt"
"math/big"
"strings"
@@ -94,7 +93,6 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
out, _ = sjson.Set(out, "stream", stream)
// Process contents (Gemini messages) -> OpenAI messages
- var openAIMessages []interface{}
var toolCallIDs []string // Track tool call IDs for matching with tool results
// System instruction -> OpenAI system message
@@ -105,22 +103,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
}
if systemInstruction.Exists() {
parts := systemInstruction.Get("parts")
- msg := map[string]interface{}{
- "role": "system",
- "content": []interface{}{},
- }
-
- var aggregatedParts []interface{}
+ msg := `{"role":"system","content":[]}`
+ hasContent := false
if parts.Exists() && parts.IsArray() {
parts.ForEach(func(_, part gjson.Result) bool {
// Handle text parts
if text := part.Get("text"); text.Exists() {
- formattedText := text.String()
- aggregatedParts = append(aggregatedParts, map[string]interface{}{
- "type": "text",
- "text": formattedText,
- })
+ contentPart := `{"type":"text","text":""}`
+ contentPart, _ = sjson.Set(contentPart, "text", text.String())
+ msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
+ hasContent = true
}
// Handle inline data (e.g., images)
@@ -132,20 +125,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
data := inlineData.Get("data").String()
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
- aggregatedParts = append(aggregatedParts, map[string]interface{}{
- "type": "image_url",
- "image_url": map[string]interface{}{
- "url": imageURL,
- },
- })
+ contentPart := `{"type":"image_url","image_url":{"url":""}}`
+ contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
+ msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
+ hasContent = true
}
return true
})
}
- if len(aggregatedParts) > 0 {
- msg["content"] = aggregatedParts
- openAIMessages = append(openAIMessages, msg)
+ if hasContent {
+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
}
}
@@ -159,16 +149,15 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
role = "assistant"
}
- // Create OpenAI message
- msg := map[string]interface{}{
- "role": role,
- "content": "",
- }
+ msg := `{"role":"","content":""}`
+ msg, _ = sjson.Set(msg, "role", role)
var textBuilder strings.Builder
- var aggregatedParts []interface{}
+ contentWrapper := `{"arr":[]}`
+ contentPartsCount := 0
onlyTextContent := true
- var toolCalls []interface{}
+ toolCallsWrapper := `{"arr":[]}`
+ toolCallsCount := 0
if parts.Exists() && parts.IsArray() {
parts.ForEach(func(_, part gjson.Result) bool {
@@ -176,10 +165,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
if text := part.Get("text"); text.Exists() {
formattedText := text.String()
textBuilder.WriteString(formattedText)
- aggregatedParts = append(aggregatedParts, map[string]interface{}{
- "type": "text",
- "text": formattedText,
- })
+ contentPart := `{"type":"text","text":""}`
+ contentPart, _ = sjson.Set(contentPart, "text", formattedText)
+ contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
+ contentPartsCount++
}
// Handle inline data (e.g., images)
@@ -193,12 +182,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
data := inlineData.Get("data").String()
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
- aggregatedParts = append(aggregatedParts, map[string]interface{}{
- "type": "image_url",
- "image_url": map[string]interface{}{
- "url": imageURL,
- },
- })
+ contentPart := `{"type":"image_url","image_url":{"url":""}}`
+ contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
+ contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
+ contentPartsCount++
}
// Handle function calls (Gemini) -> tool calls (OpenAI)
@@ -206,44 +193,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
toolCallID := genToolCallID()
toolCallIDs = append(toolCallIDs, toolCallID)
- toolCall := map[string]interface{}{
- "id": toolCallID,
- "type": "function",
- "function": map[string]interface{}{
- "name": functionCall.Get("name").String(),
- },
- }
+ toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
+ toolCall, _ = sjson.Set(toolCall, "id", toolCallID)
+ toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String())
// Convert args to arguments JSON string
if args := functionCall.Get("args"); args.Exists() {
- argsJSON, _ := json.Marshal(args.Value())
- toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
+ toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw)
} else {
- toolCall["function"].(map[string]interface{})["arguments"] = "{}"
+ toolCall, _ = sjson.Set(toolCall, "function.arguments", "{}")
}
- toolCalls = append(toolCalls, toolCall)
+ toolCallsWrapper, _ = sjson.SetRaw(toolCallsWrapper, "arr.-1", toolCall)
+ toolCallsCount++
}
// Handle function responses (Gemini) -> tool role messages (OpenAI)
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
// Create tool message for function response
- toolMsg := map[string]interface{}{
- "role": "tool",
- "tool_call_id": "", // Will be set based on context
- "content": "",
- }
+ toolMsg := `{"role":"tool","tool_call_id":"","content":""}`
// Convert response.content to JSON string
if response := functionResponse.Get("response"); response.Exists() {
- if content = response.Get("content"); content.Exists() {
- // Use the content field from the response
- contentJSON, _ := json.Marshal(content.Value())
- toolMsg["content"] = string(contentJSON)
+ if contentField := response.Get("content"); contentField.Exists() {
+ toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw)
} else {
- // Fallback to entire response
- responseJSON, _ := json.Marshal(response.Value())
- toolMsg["content"] = string(responseJSON)
+ toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw)
}
}
@@ -252,13 +227,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
if len(toolCallIDs) > 0 {
// Use the last tool call ID (simple matching by function name)
// In a real implementation, you might want more sophisticated matching
- toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
+ toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1])
} else {
// Generate a tool call ID if none available
- toolMsg["tool_call_id"] = genToolCallID()
+ toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", genToolCallID())
}
- openAIMessages = append(openAIMessages, toolMsg)
+ out, _ = sjson.SetRaw(out, "messages.-1", toolMsg)
}
return true
@@ -266,170 +241,46 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
}
// Set content
- if len(aggregatedParts) > 0 {
+ if contentPartsCount > 0 {
if onlyTextContent {
- msg["content"] = textBuilder.String()
+ msg, _ = sjson.Set(msg, "content", textBuilder.String())
} else {
- msg["content"] = aggregatedParts
+ msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw)
}
}
// Set tool calls if any
- if len(toolCalls) > 0 {
- msg["tool_calls"] = toolCalls
+ if toolCallsCount > 0 {
+ msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw)
}
- openAIMessages = append(openAIMessages, msg)
-
- // switch role {
- // case "user", "model":
- // // Convert role: model -> assistant
- // if role == "model" {
- // role = "assistant"
- // }
- //
- // // Create OpenAI message
- // msg := map[string]interface{}{
- // "role": role,
- // "content": "",
- // }
- //
- // var contentParts []string
- // var toolCalls []interface{}
- //
- // if parts.Exists() && parts.IsArray() {
- // parts.ForEach(func(_, part gjson.Result) bool {
- // // Handle text parts
- // if text := part.Get("text"); text.Exists() {
- // contentParts = append(contentParts, text.String())
- // }
- //
- // // Handle function calls (Gemini) -> tool calls (OpenAI)
- // if functionCall := part.Get("functionCall"); functionCall.Exists() {
- // toolCallID := genToolCallID()
- // toolCallIDs = append(toolCallIDs, toolCallID)
- //
- // toolCall := map[string]interface{}{
- // "id": toolCallID,
- // "type": "function",
- // "function": map[string]interface{}{
- // "name": functionCall.Get("name").String(),
- // },
- // }
- //
- // // Convert args to arguments JSON string
- // if args := functionCall.Get("args"); args.Exists() {
- // argsJSON, _ := json.Marshal(args.Value())
- // toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
- // } else {
- // toolCall["function"].(map[string]interface{})["arguments"] = "{}"
- // }
- //
- // toolCalls = append(toolCalls, toolCall)
- // }
- //
- // return true
- // })
- // }
- //
- // // Set content
- // if len(contentParts) > 0 {
- // msg["content"] = strings.Join(contentParts, "")
- // }
- //
- // // Set tool calls if any
- // if len(toolCalls) > 0 {
- // msg["tool_calls"] = toolCalls
- // }
- //
- // openAIMessages = append(openAIMessages, msg)
- //
- // case "function":
- // // Handle Gemini function role -> OpenAI tool role
- // if parts.Exists() && parts.IsArray() {
- // parts.ForEach(func(_, part gjson.Result) bool {
- // // Handle function responses (Gemini) -> tool role messages (OpenAI)
- // if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
- // // Create tool message for function response
- // toolMsg := map[string]interface{}{
- // "role": "tool",
- // "tool_call_id": "", // Will be set based on context
- // "content": "",
- // }
- //
- // // Convert response.content to JSON string
- // if response := functionResponse.Get("response"); response.Exists() {
- // if content = response.Get("content"); content.Exists() {
- // // Use the content field from the response
- // contentJSON, _ := json.Marshal(content.Value())
- // toolMsg["content"] = string(contentJSON)
- // } else {
- // // Fallback to entire response
- // responseJSON, _ := json.Marshal(response.Value())
- // toolMsg["content"] = string(responseJSON)
- // }
- // }
- //
- // // Try to match with previous tool call ID
- // _ = functionResponse.Get("name").String() // functionName not used for now
- // if len(toolCallIDs) > 0 {
- // // Use the last tool call ID (simple matching by function name)
- // // In a real implementation, you might want more sophisticated matching
- // toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
- // } else {
- // // Generate a tool call ID if none available
- // toolMsg["tool_call_id"] = genToolCallID()
- // }
- //
- // openAIMessages = append(openAIMessages, toolMsg)
- // }
- //
- // return true
- // })
- // }
- // }
+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
return true
})
}
- // Set messages
- if len(openAIMessages) > 0 {
- messagesJSON, _ := json.Marshal(openAIMessages)
- out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
- }
-
// Tools mapping: Gemini tools -> OpenAI tools
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
- var openAITools []interface{}
tools.ForEach(func(_, tool gjson.Result) bool {
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
- openAITool := map[string]interface{}{
- "type": "function",
- "function": map[string]interface{}{
- "name": funcDecl.Get("name").String(),
- "description": funcDecl.Get("description").String(),
- },
- }
+ openAITool := `{"type":"function","function":{"name":"","description":""}}`
+ openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String())
+ openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String())
// Convert parameters schema
if parameters := funcDecl.Get("parameters"); parameters.Exists() {
- openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
- } else if parameters = funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
- openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
+ openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
+ } else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
+ openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
}
- openAITools = append(openAITools, openAITool)
+ out, _ = sjson.SetRaw(out, "tools.-1", openAITool)
return true
})
}
return true
})
-
- if len(openAITools) > 0 {
- toolsJSON, _ := json.Marshal(openAITools)
- out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
- }
}
// Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it)
diff --git a/internal/translator/openai/gemini/openai_gemini_response.go b/internal/translator/openai/gemini/openai_gemini_response.go
index aac33561..040f805c 100644
--- a/internal/translator/openai/gemini/openai_gemini_response.go
+++ b/internal/translator/openai/gemini/openai_gemini_response.go
@@ -8,7 +8,6 @@ package gemini
import (
"bytes"
"context"
- "encoding/json"
"fmt"
"strconv"
"strings"
@@ -84,15 +83,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
template, _ = sjson.Set(template, "model", model.String())
}
- usageObj := map[string]interface{}{
- "promptTokenCount": usage.Get("prompt_tokens").Int(),
- "candidatesTokenCount": usage.Get("completion_tokens").Int(),
- "totalTokenCount": usage.Get("total_tokens").Int(),
- }
+ template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
+ template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
+ template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
- usageObj["thoughtsTokenCount"] = reasoningTokens
+ template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
}
- template, _ = sjson.Set(template, "usageMetadata", usageObj)
return []string{template}
}
return []string{}
@@ -133,13 +129,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
continue
}
reasoningTemplate := baseTemplate
- parts := []interface{}{
- map[string]interface{}{
- "thought": true,
- "text": reasoningText,
- },
- }
- reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts", parts)
+ reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
+ reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
chunkOutputs = append(chunkOutputs, reasoningTemplate)
}
}
@@ -150,13 +141,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
// Create text part for this delta
- parts := []interface{}{
- map[string]interface{}{
- "text": contentText,
- },
- }
contentTemplate := baseTemplate
- contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts", parts)
+ contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
chunkOutputs = append(chunkOutputs, contentTemplate)
}
@@ -225,24 +211,13 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
// If we have accumulated tool calls, output them now
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
- var parts []interface{}
+ partIndex := 0
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
- argsStr := accumulator.Arguments.String()
- var argsMap map[string]interface{}
-
- argsMap = parseArgsToMap(argsStr)
-
- functionCallPart := map[string]interface{}{
- "functionCall": map[string]interface{}{
- "name": accumulator.Name,
- "args": argsMap,
- },
- }
- parts = append(parts, functionCallPart)
- }
-
- if len(parts) > 0 {
- template, _ = sjson.Set(template, "candidates.0.content.parts", parts)
+ namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
+ argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
+ template, _ = sjson.Set(template, namePath, accumulator.Name)
+ template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
+ partIndex++
}
// Clear accumulators
@@ -255,15 +230,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
// Handle usage information
if usage := root.Get("usage"); usage.Exists() {
- usageObj := map[string]interface{}{
- "promptTokenCount": usage.Get("prompt_tokens").Int(),
- "candidatesTokenCount": usage.Get("completion_tokens").Int(),
- "totalTokenCount": usage.Get("total_tokens").Int(),
- }
+ template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
+ template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
+ template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
- usageObj["thoughtsTokenCount"] = reasoningTokens
+ template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
}
- template, _ = sjson.Set(template, "usageMetadata", usageObj)
results = append(results, template)
return true
}
@@ -291,46 +263,54 @@ func mapOpenAIFinishReasonToGemini(openAIReason string) string {
}
}
-// parseArgsToMap safely parses a JSON string of function arguments into a map.
-// It returns an empty map if the input is empty or cannot be parsed as a JSON object.
-func parseArgsToMap(argsStr string) map[string]interface{} {
+// parseArgsToObjectRaw safely parses a JSON string of function arguments into an object JSON string.
+// It returns "{}" if the input is empty or cannot be parsed as a JSON object.
+func parseArgsToObjectRaw(argsStr string) string {
trimmed := strings.TrimSpace(argsStr)
if trimmed == "" || trimmed == "{}" {
- return map[string]interface{}{}
+ return "{}"
}
// First try strict JSON
- var out map[string]interface{}
- if errUnmarshal := json.Unmarshal([]byte(trimmed), &out); errUnmarshal == nil {
- return out
+ if gjson.Valid(trimmed) {
+ strict := gjson.Parse(trimmed)
+ if strict.IsObject() {
+ return strict.Raw
+ }
}
// Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)
- tolerant := tolerantParseJSONMap(trimmed)
- if len(tolerant) > 0 {
+ tolerant := tolerantParseJSONObjectRaw(trimmed)
+ if tolerant != "{}" {
return tolerant
}
// Fallback: return empty object when parsing fails
- return map[string]interface{}{}
+ return "{}"
}
-// tolerantParseJSONMap attempts to parse a JSON-like object string into a map, tolerating
+func escapeSjsonPathKey(key string) string {
+ key = strings.ReplaceAll(key, `\`, `\\`)
+ key = strings.ReplaceAll(key, `.`, `\.`)
+ return key
+}
+
+// tolerantParseJSONObjectRaw attempts to parse a JSON-like object string into a JSON object string, tolerating
// bareword values (unquoted strings) commonly seen during streamed tool calls.
// Example input: {"location": 北京, "unit": celsius}
-func tolerantParseJSONMap(s string) map[string]interface{} {
+func tolerantParseJSONObjectRaw(s string) string {
// Ensure we operate within the outermost braces if present
start := strings.Index(s, "{")
end := strings.LastIndex(s, "}")
if start == -1 || end == -1 || start >= end {
- return map[string]interface{}{}
+ return "{}"
}
content := s[start+1 : end]
runes := []rune(content)
n := len(runes)
i := 0
- result := make(map[string]interface{})
+ result := "{}"
for i < n {
// Skip whitespace and commas
@@ -356,6 +336,7 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
break
}
keyName := jsonStringTokenToRawString(keyToken)
+ sjsonKey := escapeSjsonPathKey(keyName)
i = nextIdx
// Skip whitespace
@@ -375,17 +356,16 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
}
// Parse value (string, number, object/array, bareword)
- var value interface{}
switch runes[i] {
case '"':
// JSON string
valToken, ni := parseJSONStringRunes(runes, i)
if ni == -1 {
// Malformed; treat as empty string
- value = ""
+ result, _ = sjson.Set(result, sjsonKey, "")
i = n
} else {
- value = jsonStringTokenToRawString(valToken)
+ result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
i = ni
}
case '{', '[':
@@ -394,11 +374,10 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
if ni == -1 {
i = n
} else {
- var anyVal interface{}
- if errUnmarshal := json.Unmarshal([]byte(seg), &anyVal); errUnmarshal == nil {
- value = anyVal
+ if gjson.Valid(seg) {
+ result, _ = sjson.SetRaw(result, sjsonKey, seg)
} else {
- value = seg
+ result, _ = sjson.Set(result, sjsonKey, seg)
}
i = ni
}
@@ -411,21 +390,19 @@ func tolerantParseJSONMap(s string) map[string]interface{} {
token := strings.TrimSpace(string(runes[i:j]))
// Interpret common JSON atoms and numbers; otherwise treat as string
if token == "true" {
- value = true
+ result, _ = sjson.Set(result, sjsonKey, true)
} else if token == "false" {
- value = false
+ result, _ = sjson.Set(result, sjsonKey, false)
} else if token == "null" {
- value = nil
+ result, _ = sjson.Set(result, sjsonKey, nil)
} else if numVal, ok := tryParseNumber(token); ok {
- value = numVal
+ result, _ = sjson.Set(result, sjsonKey, numVal)
} else {
- value = token
+ result, _ = sjson.Set(result, sjsonKey, token)
}
i = j
}
- result[keyName] = value
-
// Skip trailing whitespace and optional comma before next pair
for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {
i++
@@ -463,9 +440,9 @@ func parseJSONStringRunes(runes []rune, start int) (string, int) {
// jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.
func jsonStringTokenToRawString(token string) string {
- var s string
- if errUnmarshal := json.Unmarshal([]byte(token), &s); errUnmarshal == nil {
- return s
+ r := gjson.Parse(token)
+ if r.Type == gjson.String {
+ return r.String()
}
// Fallback: strip surrounding quotes if present
if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {
@@ -579,7 +556,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
}
}
- var parts []interface{}
+ partIndex := 0
// Handle reasoning content before visible text
if reasoning := message.Get("reasoning_content"); reasoning.Exists() {
@@ -587,18 +564,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
if reasoningText == "" {
continue
}
- parts = append(parts, map[string]interface{}{
- "thought": true,
- "text": reasoningText,
- })
+ out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
+ out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
+ partIndex++
}
}
// Handle content first
if content := message.Get("content"); content.Exists() && content.String() != "" {
- parts = append(parts, map[string]interface{}{
- "text": content.String(),
- })
+ out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
+ partIndex++
}
// Handle tool calls
@@ -609,27 +584,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
functionName := function.Get("name").String()
functionArgs := function.Get("arguments").String()
- // Parse arguments
- var argsMap map[string]interface{}
- argsMap = parseArgsToMap(functionArgs)
-
- functionCallPart := map[string]interface{}{
- "functionCall": map[string]interface{}{
- "name": functionName,
- "args": argsMap,
- },
- }
- parts = append(parts, functionCallPart)
+ namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
+ argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
+ out, _ = sjson.Set(out, namePath, functionName)
+ out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
+ partIndex++
}
return true
})
}
- // Set parts
- if len(parts) > 0 {
- out, _ = sjson.Set(out, "candidates.0.content.parts", parts)
- }
-
// Handle finish reason
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
@@ -645,15 +609,12 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
// Handle usage information
if usage := root.Get("usage"); usage.Exists() {
- usageObj := map[string]interface{}{
- "promptTokenCount": usage.Get("prompt_tokens").Int(),
- "candidatesTokenCount": usage.Get("completion_tokens").Int(),
- "totalTokenCount": usage.Get("total_tokens").Int(),
- }
+ out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
+ out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
+ out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
- usageObj["thoughtsTokenCount"] = reasoningTokens
+ out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
}
- out, _ = sjson.Set(out, "usageMetadata", usageObj)
}
return out
diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go
index 2bda2029..17233ca5 100644
--- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go
+++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go
@@ -484,16 +484,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
}
}
// Build response.output using aggregated buffers
- var outputs []interface{}
+ outputsWrapper := `{"arr":[]}`
if st.ReasoningBuf.Len() > 0 {
- outputs = append(outputs, map[string]interface{}{
- "id": st.ReasoningID,
- "type": "reasoning",
- "summary": []interface{}{map[string]interface{}{
- "type": "summary_text",
- "text": st.ReasoningBuf.String(),
- }},
- })
+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
+ item, _ = sjson.Set(item, "id", st.ReasoningID)
+ item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
// Append message items in ascending index order
if len(st.MsgItemAdded) > 0 {
@@ -513,18 +509,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
if b := st.MsgTextBuf[i]; b != nil {
txt = b.String()
}
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("msg_%s_%d", st.ResponseID, i),
- "type": "message",
- "status": "completed",
- "content": []interface{}{map[string]interface{}{
- "type": "output_text",
- "annotations": []interface{}{},
- "logprobs": []interface{}{},
- "text": txt,
- }},
- "role": "assistant",
- })
+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
+ item, _ = sjson.Set(item, "content.0.text", txt)
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
}
if len(st.FuncArgsBuf) > 0 {
@@ -547,18 +535,16 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
}
callID := st.FuncCallIDs[i]
name := st.FuncNames[i]
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("fc_%s", callID),
- "type": "function_call",
- "status": "completed",
- "arguments": args,
- "call_id": callID,
- "name": name,
- })
+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
+ item, _ = sjson.Set(item, "arguments", args)
+ item, _ = sjson.Set(item, "call_id", callID)
+ item, _ = sjson.Set(item, "name", name)
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
}
- if len(outputs) > 0 {
- completed, _ = sjson.Set(completed, "response.output", outputs)
+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
+ completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
}
if st.UsageSeen {
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
@@ -681,7 +667,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
}
// Build output list from choices[...]
- var outputs []interface{}
+ outputsWrapper := `{"arr":[]}`
// Detect and capture reasoning content if present
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
includeReasoning := rcText != ""
@@ -693,21 +679,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
if strings.HasPrefix(rid, "resp_") {
rid = strings.TrimPrefix(rid, "resp_")
}
- reasoningItem := map[string]interface{}{
- "id": fmt.Sprintf("rs_%s", rid),
- "type": "reasoning",
- "encrypted_content": "",
- }
// Prefer summary_text from reasoning_content; encrypted_content is optional
- var summaries []interface{}
+ reasoningItem := `{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`
+ reasoningItem, _ = sjson.Set(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
if rcText != "" {
- summaries = append(summaries, map[string]interface{}{
- "type": "summary_text",
- "text": rcText,
- })
+ reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text")
+ reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText)
}
- reasoningItem["summary"] = summaries
- outputs = append(outputs, reasoningItem)
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem)
}
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
@@ -716,18 +695,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
if msg.Exists() {
// Text message part
if c := msg.Get("content"); c.Exists() && c.String() != "" {
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())),
- "type": "message",
- "status": "completed",
- "content": []interface{}{map[string]interface{}{
- "type": "output_text",
- "annotations": []interface{}{},
- "logprobs": []interface{}{},
- "text": c.String(),
- }},
- "role": "assistant",
- })
+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
+ item, _ = sjson.Set(item, "content.0.text", c.String())
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
}
// Function/tool calls
@@ -736,14 +707,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
callID := tc.Get("id").String()
name := tc.Get("function.name").String()
args := tc.Get("function.arguments").String()
- outputs = append(outputs, map[string]interface{}{
- "id": fmt.Sprintf("fc_%s", callID),
- "type": "function_call",
- "status": "completed",
- "arguments": args,
- "call_id": callID,
- "name": name,
- })
+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
+ item, _ = sjson.Set(item, "arguments", args)
+ item, _ = sjson.Set(item, "call_id", callID)
+ item, _ = sjson.Set(item, "name", name)
+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
return true
})
}
@@ -751,8 +720,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
return true
})
}
- if len(outputs) > 0 {
- resp, _ = sjson.Set(resp, "output", outputs)
+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
+ resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw)
}
// usage mapping
diff --git a/sdk/api/handlers/openai/openai_handlers.go b/sdk/api/handlers/openai/openai_handlers.go
index 485afe4d..ae925f91 100644
--- a/sdk/api/handlers/openai/openai_handlers.go
+++ b/sdk/api/handlers/openai/openai_handlers.go
@@ -17,6 +17,7 @@ import (
. "github.com/router-for-me/CLIProxyAPI/v6/internal/constant"
"github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
+ responsesconverter "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses"
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
@@ -109,7 +110,17 @@ func (h *OpenAIAPIHandler) ChatCompletions(c *gin.Context) {
// Check if the client requested a streaming response.
streamResult := gjson.GetBytes(rawJSON, "stream")
- if streamResult.Type == gjson.True {
+ stream := streamResult.Type == gjson.True
+
+ // Some clients send OpenAI Responses-format payloads to /v1/chat/completions.
+ // Convert them to Chat Completions so downstream translators preserve tool metadata.
+ if shouldTreatAsResponsesFormat(rawJSON) {
+ modelName := gjson.GetBytes(rawJSON, "model").String()
+ rawJSON = responsesconverter.ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName, rawJSON, stream)
+ stream = gjson.GetBytes(rawJSON, "stream").Bool()
+ }
+
+ if stream {
h.handleStreamingResponse(c, rawJSON)
} else {
h.handleNonStreamingResponse(c, rawJSON)
@@ -117,6 +128,21 @@ func (h *OpenAIAPIHandler) ChatCompletions(c *gin.Context) {
}
+// shouldTreatAsResponsesFormat detects OpenAI Responses-style payloads that are
+// accidentally sent to the Chat Completions endpoint.
+func shouldTreatAsResponsesFormat(rawJSON []byte) bool {
+ if gjson.GetBytes(rawJSON, "messages").Exists() {
+ return false
+ }
+ if gjson.GetBytes(rawJSON, "input").Exists() {
+ return true
+ }
+ if gjson.GetBytes(rawJSON, "instructions").Exists() {
+ return true
+ }
+ return false
+}
+
// Completions handles the /v1/completions endpoint.
// It determines whether the request is for a streaming or non-streaming response
// and calls the appropriate handler based on the model provider.
diff --git a/sdk/cliproxy/auth/manager.go b/sdk/cliproxy/auth/manager.go
index c345cd15..38d4c0fa 100644
--- a/sdk/cliproxy/auth/manager.go
+++ b/sdk/cliproxy/auth/manager.go
@@ -135,6 +135,18 @@ func NewManager(store Store, selector Selector, hook Hook) *Manager {
}
}
+func (m *Manager) SetSelector(selector Selector) {
+ if m == nil {
+ return
+ }
+ if selector == nil {
+ selector = &RoundRobinSelector{}
+ }
+ m.mu.Lock()
+ m.selector = selector
+ m.mu.Unlock()
+}
+
// SetStore swaps the underlying persistence store.
func (m *Manager) SetStore(store Store) {
m.mu.Lock()
diff --git a/sdk/cliproxy/auth/selector.go b/sdk/cliproxy/auth/selector.go
index d4edc8bd..d7e120c5 100644
--- a/sdk/cliproxy/auth/selector.go
+++ b/sdk/cliproxy/auth/selector.go
@@ -20,6 +20,11 @@ type RoundRobinSelector struct {
cursors map[string]int
}
+// FillFirstSelector selects the first available credential (deterministic ordering).
+// This "burns" one account before moving to the next, which can help stagger
+// rolling-window subscription caps (e.g. chat message limits).
+type FillFirstSelector struct{}
+
type blockReason int
const (
@@ -98,20 +103,8 @@ func (e *modelCooldownError) Headers() http.Header {
return headers
}
-// Pick selects the next available auth for the provider in a round-robin manner.
-func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) {
- _ = ctx
- _ = opts
- if len(auths) == 0 {
- return nil, &Error{Code: "auth_not_found", Message: "no auth candidates"}
- }
- if s.cursors == nil {
- s.cursors = make(map[string]int)
- }
- available := make([]*Auth, 0, len(auths))
- now := time.Now()
- cooldownCount := 0
- var earliest time.Time
+func collectAvailable(auths []*Auth, model string, now time.Time) (available []*Auth, cooldownCount int, earliest time.Time) {
+ available = make([]*Auth, 0, len(auths))
for i := 0; i < len(auths); i++ {
candidate := auths[i]
blocked, reason, next := isAuthBlockedForModel(candidate, model, now)
@@ -126,6 +119,18 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
}
}
}
+ if len(available) > 1 {
+ sort.Slice(available, func(i, j int) bool { return available[i].ID < available[j].ID })
+ }
+ return available, cooldownCount, earliest
+}
+
+func getAvailableAuths(auths []*Auth, provider, model string, now time.Time) ([]*Auth, error) {
+ if len(auths) == 0 {
+ return nil, &Error{Code: "auth_not_found", Message: "no auth candidates"}
+ }
+
+ available, cooldownCount, earliest := collectAvailable(auths, model, now)
if len(available) == 0 {
if cooldownCount == len(auths) && !earliest.IsZero() {
resetIn := earliest.Sub(now)
@@ -136,12 +141,24 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
}
return nil, &Error{Code: "auth_unavailable", Message: "no auth available"}
}
- // Make round-robin deterministic even if caller's candidate order is unstable.
- if len(available) > 1 {
- sort.Slice(available, func(i, j int) bool { return available[i].ID < available[j].ID })
+
+ return available, nil
+}
+
+// Pick selects the next available auth for the provider in a round-robin manner.
+func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) {
+ _ = ctx
+ _ = opts
+ now := time.Now()
+ available, err := getAvailableAuths(auths, provider, model, now)
+ if err != nil {
+ return nil, err
}
key := provider + ":" + model
s.mu.Lock()
+ if s.cursors == nil {
+ s.cursors = make(map[string]int)
+ }
index := s.cursors[key]
if index >= 2_147_483_640 {
@@ -154,6 +171,18 @@ func (s *RoundRobinSelector) Pick(ctx context.Context, provider, model string, o
return available[index%len(available)], nil
}
+// Pick selects the first available auth for the provider in a deterministic manner.
+func (s *FillFirstSelector) Pick(ctx context.Context, provider, model string, opts cliproxyexecutor.Options, auths []*Auth) (*Auth, error) {
+ _ = ctx
+ _ = opts
+ now := time.Now()
+ available, err := getAvailableAuths(auths, provider, model, now)
+ if err != nil {
+ return nil, err
+ }
+ return available[0], nil
+}
+
func isAuthBlockedForModel(auth *Auth, model string, now time.Time) (bool, blockReason, time.Time) {
if auth == nil {
return true, blockReasonOther, time.Time{}
diff --git a/sdk/cliproxy/auth/selector_test.go b/sdk/cliproxy/auth/selector_test.go
new file mode 100644
index 00000000..f4beed03
--- /dev/null
+++ b/sdk/cliproxy/auth/selector_test.go
@@ -0,0 +1,113 @@
+package auth
+
+import (
+ "context"
+ "errors"
+ "sync"
+ "testing"
+
+ cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
+)
+
+func TestFillFirstSelectorPick_Deterministic(t *testing.T) {
+ t.Parallel()
+
+ selector := &FillFirstSelector{}
+ auths := []*Auth{
+ {ID: "b"},
+ {ID: "a"},
+ {ID: "c"},
+ }
+
+ got, err := selector.Pick(context.Background(), "gemini", "", cliproxyexecutor.Options{}, auths)
+ if err != nil {
+ t.Fatalf("Pick() error = %v", err)
+ }
+ if got == nil {
+ t.Fatalf("Pick() auth = nil")
+ }
+ if got.ID != "a" {
+ t.Fatalf("Pick() auth.ID = %q, want %q", got.ID, "a")
+ }
+}
+
+func TestRoundRobinSelectorPick_CyclesDeterministic(t *testing.T) {
+ t.Parallel()
+
+ selector := &RoundRobinSelector{}
+ auths := []*Auth{
+ {ID: "b"},
+ {ID: "a"},
+ {ID: "c"},
+ }
+
+ want := []string{"a", "b", "c", "a", "b"}
+ for i, id := range want {
+ got, err := selector.Pick(context.Background(), "gemini", "", cliproxyexecutor.Options{}, auths)
+ if err != nil {
+ t.Fatalf("Pick() #%d error = %v", i, err)
+ }
+ if got == nil {
+ t.Fatalf("Pick() #%d auth = nil", i)
+ }
+ if got.ID != id {
+ t.Fatalf("Pick() #%d auth.ID = %q, want %q", i, got.ID, id)
+ }
+ }
+}
+
+func TestRoundRobinSelectorPick_Concurrent(t *testing.T) {
+ selector := &RoundRobinSelector{}
+ auths := []*Auth{
+ {ID: "b"},
+ {ID: "a"},
+ {ID: "c"},
+ }
+
+ start := make(chan struct{})
+ var wg sync.WaitGroup
+ errCh := make(chan error, 1)
+
+ goroutines := 32
+ iterations := 100
+ for i := 0; i < goroutines; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ <-start
+ for j := 0; j < iterations; j++ {
+ got, err := selector.Pick(context.Background(), "gemini", "", cliproxyexecutor.Options{}, auths)
+ if err != nil {
+ select {
+ case errCh <- err:
+ default:
+ }
+ return
+ }
+ if got == nil {
+ select {
+ case errCh <- errors.New("Pick() returned nil auth"):
+ default:
+ }
+ return
+ }
+ if got.ID == "" {
+ select {
+ case errCh <- errors.New("Pick() returned auth with empty ID"):
+ default:
+ }
+ return
+ }
+ }
+ }()
+ }
+
+ close(start)
+ wg.Wait()
+
+ select {
+ case err := <-errCh:
+ t.Fatalf("concurrent Pick() error = %v", err)
+ default:
+ }
+}
diff --git a/sdk/cliproxy/builder.go b/sdk/cliproxy/builder.go
index a85e91d9..381a0926 100644
--- a/sdk/cliproxy/builder.go
+++ b/sdk/cliproxy/builder.go
@@ -5,6 +5,7 @@ package cliproxy
import (
"fmt"
+ "strings"
"github.com/router-for-me/CLIProxyAPI/v6/internal/api"
sdkaccess "github.com/router-for-me/CLIProxyAPI/v6/sdk/access"
@@ -197,7 +198,20 @@ func (b *Builder) Build() (*Service, error) {
if dirSetter, ok := tokenStore.(interface{ SetBaseDir(string) }); ok && b.cfg != nil {
dirSetter.SetBaseDir(b.cfg.AuthDir)
}
- coreManager = coreauth.NewManager(tokenStore, nil, nil)
+
+ strategy := ""
+ if b.cfg != nil {
+ strategy = strings.ToLower(strings.TrimSpace(b.cfg.Routing.Strategy))
+ }
+ var selector coreauth.Selector
+ switch strategy {
+ case "fill-first", "fillfirst", "ff":
+ selector = &coreauth.FillFirstSelector{}
+ default:
+ selector = &coreauth.RoundRobinSelector{}
+ }
+
+ coreManager = coreauth.NewManager(tokenStore, selector, nil)
}
// Attach a default RoundTripper provider so providers can opt-in per-auth transports.
coreManager.SetRoundTripperProvider(newDefaultRoundTripperProvider())
diff --git a/sdk/cliproxy/service.go b/sdk/cliproxy/service.go
index e4cd9e5d..a699ca61 100644
--- a/sdk/cliproxy/service.go
+++ b/sdk/cliproxy/service.go
@@ -506,6 +506,13 @@ func (s *Service) Run(ctx context.Context) error {
var watcherWrapper *WatcherWrapper
reloadCallback := func(newCfg *config.Config) {
+ previousStrategy := ""
+ s.cfgMu.RLock()
+ if s.cfg != nil {
+ previousStrategy = strings.ToLower(strings.TrimSpace(s.cfg.Routing.Strategy))
+ }
+ s.cfgMu.RUnlock()
+
if newCfg == nil {
s.cfgMu.RLock()
newCfg = s.cfg
@@ -514,6 +521,30 @@ func (s *Service) Run(ctx context.Context) error {
if newCfg == nil {
return
}
+
+ nextStrategy := strings.ToLower(strings.TrimSpace(newCfg.Routing.Strategy))
+ normalizeStrategy := func(strategy string) string {
+ switch strategy {
+ case "fill-first", "fillfirst", "ff":
+ return "fill-first"
+ default:
+ return "round-robin"
+ }
+ }
+ previousStrategy = normalizeStrategy(previousStrategy)
+ nextStrategy = normalizeStrategy(nextStrategy)
+ if s.coreManager != nil && previousStrategy != nextStrategy {
+ var selector coreauth.Selector
+ switch nextStrategy {
+ case "fill-first":
+ selector = &coreauth.FillFirstSelector{}
+ default:
+ selector = &coreauth.RoundRobinSelector{}
+ }
+ s.coreManager.SetSelector(selector)
+ log.Infof("routing strategy updated to %s", nextStrategy)
+ }
+
s.applyRetryConfig(newCfg)
if s.server != nil {
s.server.UpdateClients(newCfg)