diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index ddda5ddb..ea4ba38e 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -452,7 +452,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..ead771cf 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -319,7 +319,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 +334,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 24694e1d..b699a4a0 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" @@ -171,21 +170,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..c421d22f 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,100 @@ 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 + } } - } - if len(systemInstruction.Parts) == 0 { - systemInstruction = nil + return true + }) + if hasSystemParts { + out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction) } } // 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 +142,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..5b24df5a 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 @@ -278,7 +278,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 +293,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..8c95b6ea 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,100 @@ 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 + } } - } - if len(systemInstruction.Parts) == 0 { - systemInstruction = nil + return true + }) + if hasSystemParts { + out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction) } } // 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 +135,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..af652949 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -320,7 +320,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 +335,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 a1ebc855..3c615b54 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" @@ -173,21 +172,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) } } } @@ -305,21 +297,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