History

refactor: replace `json.Marshal` and `json.Unmarshal` with `sjson` and `gjson`

Optimized the handling of JSON serialization and deserialization by replacing redundant `json.Marshal` and `json.Unmarshal` calls with `sjson` and `gjson`. Introduced a `marshalJSONValue` utility for compact JSON encoding, improving performance and code simplicity. Removed unused `encoding/json` imports.

Luis Pater committed 10 months ago

Commit a86d5011 parent dbcbe4827 files · +864 −1615Browse files

Unified · Split

27 files
  1. internal/translator/antigravity/claude/antigravity_claude_response.go +1 −1
  2. internal/translator/antigravity/gemini/antigravity_gemini_request.go +43 −65
  3. internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +2 −2
  4. internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go +3 −11
  5. internal/translator/claude/gemini/claude_gemini_request.go +4 −4
  6. internal/translator/claude/gemini/claude_gemini_response.go +28 −92
  7. internal/translator/claude/openai/chat-completions/claude_openai_request.go +44 −96
  8. internal/translator/claude/openai/chat-completions/claude_openai_response.go +46 −62
  9. internal/translator/claude/openai/responses/claude_openai-responses_request.go +9 −4
  10. internal/translator/claude/openai/responses/claude_openai-responses_response.go +34 −59
  11. internal/translator/codex/claude/codex_claude_response.go +31 −57
  12. internal/translator/codex/gemini/codex_gemini_response.go +24 −49
  13. internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +78 −83
  14. internal/translator/gemini-cli/claude/gemini-cli_claude_response.go +26 −49
  15. internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go +27 −51
  16. internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +2 −2
  17. internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +3 −11
  18. internal/translator/gemini/claude/gemini_claude_request.go +78 −83
  19. internal/translator/gemini/claude/gemini_claude_response.go +26 −49
  20. internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +2 −2
  21. internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +6 −21
  22. internal/translator/gemini/openai/responses/gemini_openai-responses_response.go +53 −72
  23. internal/translator/openai/claude/openai_claude_request.go +2 −8
  24. internal/translator/openai/claude/openai_claude_response.go +136 −307
  25. internal/translator/openai/gemini/openai_gemini_request.go +53 −202
  26. internal/translator/openai/gemini/openai_gemini_response.go +68 −107
  27. internal/translator/openai/openai/responses/openai_openai-responses_response.go +35 −66
internal/translator/antigravity/claude/antigravity_claude_response.go+1−1
@@ -452,7 +452,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or · 451 unmodified lines
452452 toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
453453 toolBlock, _ = sjson.Set(toolBlock, "name", name)
454454
455− if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) {
455+ if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) && args.IsObject() {
456456 toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)
457457 }
458458
internal/translator/antigravity/gemini/antigravity_gemini_request.go+43−65
@@ -7,7 +7,6 @@ package gemini · 6 unmodified lines
77
88 import (
99 "bytes"
10− "encoding/json"
1110 "fmt"
1211
1312 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
@@ -135,41 +134,31 @@ func ConvertGeminiRequestToAntigravity(_ string, inputRawJSON []byte, _ bool) [] · 121 unmodified lines
135134
136135 // FunctionCallGroup represents a group of function calls and their responses
137136 type FunctionCallGroup struct {
138− ModelContent map[string]interface{}
139− FunctionCalls []gjson.Result
140137 ResponsesNeeded int
141138 }
142139
143−// parseFunctionResponse attempts to unmarshal a function response part.
144−// Falls back to gjson extraction if standard json.Unmarshal fails.
145−func parseFunctionResponse(response gjson.Result) map[string]interface{} {
146− var responseMap map[string]interface{}
147− err := json.Unmarshal([]byte(response.Raw), &responseMap)
148− if err == nil {
149− return responseMap
140+// parseFunctionResponseRaw attempts to normalize a function response part into a JSON object string.
141+// Falls back to a minimal "functionResponse" object when parsing fails.
142+func parseFunctionResponseRaw(response gjson.Result) string {
143+ if response.IsObject() && gjson.Valid(response.Raw) {
144+ return response.Raw
150145 }
151146
152− log.Debugf("unmarshal function response failed, using fallback: %v", err)
147+ log.Debugf("parse function response failed, using fallback")
153148 funcResp := response.Get("functionResponse")
154149 if funcResp.Exists() {
155− fr := map[string]interface{}{
156− "name": funcResp.Get("name").String(),
157− "response": map[string]interface{}{
158− "result": funcResp.Get("response").String(),
159− },
160− }
150+ fr := `{"functionResponse":{"name":"","response":{"result":""}}}`
151+ fr, _ = sjson.Set(fr, "functionResponse.name", funcResp.Get("name").String())
152+ fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String())
161153 if id := funcResp.Get("id").String(); id != "" {
162− fr["id"] = id
154+ fr, _ = sjson.Set(fr, "functionResponse.id", id)
163155 }
164− return map[string]interface{}{"functionResponse": fr}
156+ return fr
165157 }
166158
167− return map[string]interface{}{
168− "functionResponse": map[string]interface{}{
169− "name": "unknown",
170− "response": map[string]interface{}{"result": response.String()},
171− },
172− }
159+ fr := `{"functionResponse":{"name":"unknown","response":{"result":""}}}`
160+ fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String())
161+ return fr
173162 }
174163
175164 // fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
@@ -196,7 +185,7 @@ func fixCLIToolResponse(input string) (string, error) { · 20 unmodified lines
196185 }
197186
198187 // Initialize data structures for processing and grouping
199− var newContents []interface{} // Final processed contents array
188+ contentsWrapper := `{"contents":[]}`
200189 var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
201190 var collectedResponses []gjson.Result // Standalone responses to be matched
202191
@@ -228,17 +217,16 @@ func fixCLIToolResponse(input string) (string, error) { · 25 unmodified lines
228217 collectedResponses = collectedResponses[group.ResponsesNeeded:]
229218
230219 // Create merged function response content
231− var responseParts []interface{}
220+ functionResponseContent := `{"parts":[],"role":"function"}`
232221 for _, response := range groupResponses {
233− responseParts = append(responseParts, parseFunctionResponse(response))
222+ partRaw := parseFunctionResponseRaw(response)
223+ if partRaw != "" {
224+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
225+ }
234226 }
235227
236− if len(responseParts) > 0 {
237− functionResponseContent := map[string]interface{}{
238− "parts": responseParts,
239− "role": "function",
240− }
241− newContents = append(newContents, functionResponseContent)
228+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
229+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
242230 }
243231
244232 // Remove this group as it's been satisfied
@@ -252,50 +240,42 @@ func fixCLIToolResponse(input string) (string, error) { · 7 unmodified lines
252240
253241 // If this is a model with function calls, create a new group
254242 if role == "model" {
255− var functionCallsInThisModel []gjson.Result
243+ functionCallsCount := 0
256244 parts.ForEach(func(_, part gjson.Result) bool {
257245 if part.Get("functionCall").Exists() {
258− functionCallsInThisModel = append(functionCallsInThisModel, part)
246+ functionCallsCount++
259247 }
260248 return true
261249 })
262250
263− if len(functionCallsInThisModel) > 0 {
251+ if functionCallsCount > 0 {
264252 // Add the model content
265− var contentMap map[string]interface{}
266− errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
267− if errUnmarshal != nil {
268− log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
253+ if !value.IsObject() {
254+ log.Warnf("failed to parse model content")
269255 return true
270256 }
271− newContents = append(newContents, contentMap)
257+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
272258
273259 // Create a new group for tracking responses
274260 group := &FunctionCallGroup{
275− ModelContent: contentMap,
276− FunctionCalls: functionCallsInThisModel,
277− ResponsesNeeded: len(functionCallsInThisModel),
261+ ResponsesNeeded: functionCallsCount,
278262 }
279263 pendingGroups = append(pendingGroups, group)
280264 } else {
281265 // Regular model content without function calls
282− var contentMap map[string]interface{}
283− errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
284− if errUnmarshal != nil {
285− log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
266+ if !value.IsObject() {
267+ log.Warnf("failed to parse content")
286268 return true
287269 }
288− newContents = append(newContents, contentMap)
270+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
289271 }
290272 } else {
291273 // Non-model content (user, etc.)
292− var contentMap map[string]interface{}
293− errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
294− if errUnmarshal != nil {
295− log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
274+ if !value.IsObject() {
275+ log.Warnf("failed to parse content")
296276 return true
297277 }
298− newContents = append(newContents, contentMap)
278+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
299279 }
300280
301281 return true
@@ -307,25 +287,23 @@ func fixCLIToolResponse(input string) (string, error) { · 5 unmodified lines
307287 groupResponses := collectedResponses[:group.ResponsesNeeded]
308288 collectedResponses = collectedResponses[group.ResponsesNeeded:]
309289
310− var responseParts []interface{}
290+ functionResponseContent := `{"parts":[],"role":"function"}`
311291 for _, response := range groupResponses {
312− responseParts = append(responseParts, parseFunctionResponse(response))
292+ partRaw := parseFunctionResponseRaw(response)
293+ if partRaw != "" {
294+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
295+ }
313296 }
314297
315− if len(responseParts) > 0 {
316− functionResponseContent := map[string]interface{}{
317− "parts": responseParts,
318− "role": "function",
319− }
320− newContents = append(newContents, functionResponseContent)
298+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
299+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
321300 }
322301 }
323302 }
324303
325304 // Update the original JSON with the new contents
326305 result := input
327− newContentsJSON, _ := json.Marshal(newContents)
328− result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
306+ result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
329307
330308 return result, nil
331309 }
internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go+2−2
@@ -319,7 +319,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 318 unmodified lines
319319 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
320320 continue
321321 }
322− fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
322+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
323323 if errSet != nil {
324324 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
325325 continue
@@ -334,7 +334,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 8 unmodified lines
334334 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
335335 continue
336336 }
337− fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
337+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
338338 if errSet != nil {
339339 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
340340 continue
internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go+3−11
@@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines
88 import (
99 "bytes"
1010 "context"
11− "encoding/json"
1211 "fmt"
1312 "strings"
1413 "sync/atomic"
@@ -171,21 +170,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq · 156 unmodified lines
171170 mimeType = "image/png"
172171 }
173172 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
174− imagePayload, err := json.Marshal(map[string]any{
175− "type": "image_url",
176− "image_url": map[string]string{
177− "url": imageURL,
178− },
179− })
180− if err != nil {
181− continue
182− }
173+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
174+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
183175 imagesResult := gjson.Get(template, "choices.0.delta.images")
184176 if !imagesResult.Exists() || !imagesResult.IsArray() {
185177 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
186178 }
187179 template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
188− template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
180+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
189181 }
190182 }
191183 }
internal/translator/claude/gemini/claude_gemini_request.go+4−4
@@ -194,7 +194,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream · 193 unmodified lines
194194 if name := fc.Get("name"); name.Exists() {
195195 toolUse, _ = sjson.Set(toolUse, "name", name.String())
196196 }
197− if args := fc.Get("args"); args.Exists() {
197+ if args := fc.Get("args"); args.Exists() && args.IsObject() {
198198 toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)
199199 }
200200 msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
@@ -314,11 +314,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream · 113 unmodified lines
314314 if mode := funcCalling.Get("mode"); mode.Exists() {
315315 switch mode.String() {
316316 case "AUTO":
317− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
317+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
318318 case "NONE":
319− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "none"})
319+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"none"}`)
320320 case "ANY":
321− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
321+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
322322 }
323323 }
324324 }
internal/translator/claude/gemini/claude_gemini_response.go+28−92
@@ -263,51 +263,6 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original · 262 unmodified lines
263263 }
264264 }
265265
266−// convertArrayToJSON converts []interface{} to JSON array string
267−func convertArrayToJSON(arr []interface{}) string {
268− result := "[]"
269− for _, item := range arr {
270− switch itemData := item.(type) {
271− case map[string]interface{}:
272− itemJSON := convertMapToJSON(itemData)
273− result, _ = sjson.SetRaw(result, "-1", itemJSON)
274− case string:
275− result, _ = sjson.Set(result, "-1", itemData)
276− case bool:
277− result, _ = sjson.Set(result, "-1", itemData)
278− case float64, int, int64:
279− result, _ = sjson.Set(result, "-1", itemData)
280− default:
281− result, _ = sjson.Set(result, "-1", itemData)
282− }
283− }
284− return result
285−}
286−
287−// convertMapToJSON converts map[string]interface{} to JSON object string
288−func convertMapToJSON(m map[string]interface{}) string {
289− result := "{}"
290− for key, value := range m {
291− switch val := value.(type) {
292− case map[string]interface{}:
293− nestedJSON := convertMapToJSON(val)
294− result, _ = sjson.SetRaw(result, key, nestedJSON)
295− case []interface{}:
296− arrayJSON := convertArrayToJSON(val)
297− result, _ = sjson.SetRaw(result, key, arrayJSON)
298− case string:
299− result, _ = sjson.Set(result, key, val)
300− case bool:
301− result, _ = sjson.Set(result, key, val)
302− case float64, int, int64:
303− result, _ = sjson.Set(result, key, val)
304− default:
305− result, _ = sjson.Set(result, key, val)
306− }
307− }
308− return result
309−}
310−
311266 // ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response.
312267 // This function processes the complete Claude Code response and transforms it into a single Gemini-compatible
313268 // 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, · 42 unmodified lines
356311 }
357312
358313 // Process each streaming event and collect parts
359− var allParts []interface{}
360− var finalUsage map[string]interface{}
314+ var allParts []string
315+ var finalUsageJSON string
361316 var responseID string
362317 var createdAt int64
363318
@@ -407,16 +362,14 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 43 unmodified lines
407362 if text := delta.Get("text"); text.Exists() && text.String() != "" {
408363 partJSON := `{"text":""}`
409364 partJSON, _ = sjson.Set(partJSON, "text", text.String())
410− part := gjson.Parse(partJSON).Value().(map[string]interface{})
411− allParts = append(allParts, part)
365+ allParts = append(allParts, partJSON)
412366 }
413367 case "thinking_delta":
414368 // Process reasoning/thinking content
415369 if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
416370 partJSON := `{"thought":true,"text":""}`
417371 partJSON, _ = sjson.Set(partJSON, "text", text.String())
418− part := gjson.Parse(partJSON).Value().(map[string]interface{})
419− allParts = append(allParts, part)
372+ allParts = append(allParts, partJSON)
420373 }
421374 case "input_json_delta":
422375 // accumulate args partial_json for this index
@@ -456,9 +409,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 33 unmodified lines
456409 if argsTrim != "" {
457410 functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
458411 }
459− // Parse back to interface{} for allParts
460− functionCall := gjson.Parse(functionCallJSON).Value().(map[string]interface{})
461− allParts = append(allParts, functionCall)
412+ allParts = append(allParts, functionCallJSON)
462413 // cleanup used state for this index
463414 if newParam.ToolUseArgs != nil {
464415 delete(newParam.ToolUseArgs, idx)
@@ -501,8 +452,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 36 unmodified lines
501452 // Set traffic type (required by Gemini API)
502453 usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
503454
504− // Convert to map[string]interface{} using gjson
505− finalUsage = gjson.Parse(usageJSON).Value().(map[string]interface{})
455+ finalUsageJSON = usageJSON
506456 }
507457 }
508458 }
@@ -520,12 +470,16 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 11 unmodified lines
520470
521471 // Set the consolidated parts array
522472 if len(consolidatedParts) > 0 {
523− template, _ = sjson.SetRaw(template, "candidates.0.content.parts", convertToJSONString(consolidatedParts))
473+ partsJSON := "[]"
474+ for _, partJSON := range consolidatedParts {
475+ partsJSON, _ = sjson.SetRaw(partsJSON, "-1", partJSON)
476+ }
477+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts", partsJSON)
524478 }
525479
526480 // Set usage metadata
527− if finalUsage != nil {
528− template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage))
481+ if finalUsageJSON != "" {
482+ template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON)
529483 }
530484
531485 return template
@@ -539,12 +493,12 @@ func GeminiTokenCount(ctx context.Context, count int64) string { · 7 unmodified lines
539493 // This function processes the parts array to combine adjacent text elements and thinking elements
540494 // into single consolidated parts, which results in a more readable and efficient response structure.
541495 // Tool calls and other non-text parts are preserved as separate elements.
542−func consolidateParts(parts []interface{}) []interface{} {
496+func consolidateParts(parts []string) []string {
543497 if len(parts) == 0 {
544498 return parts
545499 }
546500
547− var consolidated []interface{}
501+ var consolidated []string
548502 var currentTextPart strings.Builder
549503 var currentThoughtPart strings.Builder
550504 var hasText, hasThought bool
@@ -554,8 +508,7 @@ func consolidateParts(parts []interface{}) []interface{} { · 3 unmodified lines
554508 if hasText && currentTextPart.Len() > 0 {
555509 textPartJSON := `{"text":""}`
556510 textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
557− textPart := gjson.Parse(textPartJSON).Value().(map[string]interface{})
558− consolidated = append(consolidated, textPart)
511+ consolidated = append(consolidated, textPartJSON)
559512 currentTextPart.Reset()
560513 hasText = false
561514 }
@@ -566,42 +519,42 @@ func consolidateParts(parts []interface{}) []interface{} { · 4 unmodified lines
566519 if hasThought && currentThoughtPart.Len() > 0 {
567520 thoughtPartJSON := `{"thought":true,"text":""}`
568521 thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
569− thoughtPart := gjson.Parse(thoughtPartJSON).Value().(map[string]interface{})
570− consolidated = append(consolidated, thoughtPart)
522+ consolidated = append(consolidated, thoughtPartJSON)
571523 currentThoughtPart.Reset()
572524 hasThought = false
573525 }
574526 }
575527
576− for _, part := range parts {
577− partMap, ok := part.(map[string]interface{})
578− if !ok {
528+ for _, partJSON := range parts {
529+ part := gjson.Parse(partJSON)
530+ if !part.Exists() || !part.IsObject() {
579531 // Flush any pending parts and add this non-text part
580532 flushText()
581533 flushThought()
582− consolidated = append(consolidated, part)
534+ consolidated = append(consolidated, partJSON)
583535 continue
584536 }
585537
586− if thought, isThought := partMap["thought"]; isThought && thought == true {
538+ thought := part.Get("thought")
539+ if thought.Exists() && thought.Type == gjson.True {
587540 // This is a thinking part - flush any pending text first
588541 flushText() // Flush any pending text first
589542
590− if text, hasTextContent := partMap["text"].(string); hasTextContent {
591− currentThoughtPart.WriteString(text)
543+ if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
544+ currentThoughtPart.WriteString(text.String())
592545 hasThought = true
593546 }
594− } else if text, hasTextContent := partMap["text"].(string); hasTextContent {
547+ } else if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
595548 // This is a regular text part - flush any pending thought first
596549 flushThought() // Flush any pending thought first
597550
598− currentTextPart.WriteString(text)
551+ currentTextPart.WriteString(text.String())
599552 hasText = true
600553 } else {
601554 // This is some other type of part (like function call) - flush both text and thought
602555 flushText()
603556 flushThought()
604− consolidated = append(consolidated, part)
557+ consolidated = append(consolidated, partJSON)
605558 }
606559 }
607560
@@ -611,20 +564,3 @@ func consolidateParts(parts []interface{}) []interface{} { · 3 unmodified lines
611564
612565 return consolidated
613566 }
614−
615−// convertToJSONString converts interface{} to JSON string using sjson/gjson.
616−// This function provides a consistent way to serialize different data types to JSON strings
617−// for inclusion in the Gemini API response structure.
618−func convertToJSONString(v interface{}) string {
619− switch val := v.(type) {
620− case []interface{}:
621− return convertArrayToJSON(val)
622− case map[string]interface{}:
623− return convertMapToJSON(val)
624− default:
625− // For simple types, create a temporary JSON and extract the value
626− temp := `{"temp":null}`
627− temp, _ = sjson.Set(temp, "temp", val)
628− return gjson.Get(temp, "temp").Raw
629− }
630−}
internal/translator/claude/openai/chat-completions/claude_openai_request.go+44−96
@@ -10,7 +10,6 @@ import ( · 9 unmodified lines
1010 "crypto/rand"
1111 "crypto/sha256"
1212 "encoding/hex"
13− "encoding/json"
1413 "fmt"
1514 "math/big"
1615 "strings"
@@ -137,9 +136,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 120 unmodified lines
137136 out, _ = sjson.Set(out, "stream", stream)
138137
139138 // Process messages and transform them to Claude Code format
140− var anthropicMessages []interface{}
141− var toolCallIDs []string // Track tool call IDs for matching with tool results
142−
143139 if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
144140 messages.ForEach(func(_, message gjson.Result) bool {
145141 role := message.Get("role").String()
@@ -152,33 +148,23 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines
152148 role = "user"
153149 }
154150
155− msg := map[string]interface{}{
156− "role": role,
157− "content": []interface{}{},
158− }
151+ msg := `{"role":"","content":[]}`
152+ msg, _ = sjson.Set(msg, "role", role)
159153
160154 // Handle content based on its type (string or array)
161155 if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
162− // Simple text content conversion
163− msg["content"] = []interface{}{
164− map[string]interface{}{
165− "type": "text",
166− "text": contentResult.String(),
167− },
168− }
156+ part := `{"type":"text","text":""}`
157+ part, _ = sjson.Set(part, "text", contentResult.String())
158+ msg, _ = sjson.SetRaw(msg, "content.-1", part)
169159 } else if contentResult.Exists() && contentResult.IsArray() {
170− // Array of content parts processing
171− var contentParts []interface{}
172160 contentResult.ForEach(func(_, part gjson.Result) bool {
173161 partType := part.Get("type").String()
174162
175163 switch partType {
176164 case "text":
177− // Text part conversion
178− contentParts = append(contentParts, map[string]interface{}{
179− "type": "text",
180− "text": part.Get("text").String(),
181− })
165+ textPart := `{"type":"text","text":""}`
166+ textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
167+ msg, _ = sjson.SetRaw(msg, "content.-1", textPart)
182168
183169 case "image_url":
184170 // Convert OpenAI image format to Claude Code format
@@ -191,132 +177,95 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines
191177 mediaType := strings.TrimPrefix(mediaTypePart, "data:")
192178 data := parts[1]
193179
194− contentParts = append(contentParts, map[string]interface{}{
195− "type": "image",
196− "source": map[string]interface{}{
197− "type": "base64",
198− "media_type": mediaType,
199− "data": data,
200− },
201− })
180+ imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
181+ imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
182+ imagePart, _ = sjson.Set(imagePart, "source.data", data)
183+ msg, _ = sjson.SetRaw(msg, "content.-1", imagePart)
202184 }
203185 }
204186 }
205187 return true
206188 })
207− if len(contentParts) > 0 {
208− msg["content"] = contentParts
209− }
210− } else {
211− // Initialize empty content array for tool calls
212− msg["content"] = []interface{}{}
213189 }
214190
215191 // Handle tool calls (for assistant messages)
216192 if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() && role == "assistant" {
217− var contentParts []interface{}
218−
219− // Add existing text content if any
220− if existingContent, ok := msg["content"].([]interface{}); ok {
221− contentParts = existingContent
222− }
223−
224193 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
225194 if toolCall.Get("type").String() == "function" {
226195 toolCallID := toolCall.Get("id").String()
227196 if toolCallID == "" {
228197 toolCallID = genToolCallID()
229198 }
230− toolCallIDs = append(toolCallIDs, toolCallID)
231199
232200 function := toolCall.Get("function")
233− toolUse := map[string]interface{}{
234− "type": "tool_use",
235− "id": toolCallID,
236− "name": function.Get("name").String(),
237− }
201+ toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
202+ toolUse, _ = sjson.Set(toolUse, "id", toolCallID)
203+ toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String())
238204
239205 // Parse arguments for the tool call
240206 if args := function.Get("arguments"); args.Exists() {
241207 argsStr := args.String()
242− if argsStr != "" {
243− var argsMap map[string]interface{}
244− if err := json.Unmarshal([]byte(argsStr), &argsMap); err == nil {
245− toolUse["input"] = argsMap
208+ if argsStr != "" && gjson.Valid(argsStr) {
209+ argsJSON := gjson.Parse(argsStr)
210+ if argsJSON.IsObject() {
211+ toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
246212 } else {
247− toolUse["input"] = map[string]interface{}{}
213+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
248214 }
249215 } else {
250− toolUse["input"] = map[string]interface{}{}
216+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
251217 }
252218 } else {
253− toolUse["input"] = map[string]interface{}{}
219+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
254220 }
255221
256− contentParts = append(contentParts, toolUse)
222+ msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
257223 }
258224 return true
259225 })
260− msg["content"] = contentParts
261226 }
262227
263− anthropicMessages = append(anthropicMessages, msg)
228+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
264229
265230 case "tool":
266231 // Handle tool result messages conversion
267232 toolCallID := message.Get("tool_call_id").String()
268233 content := message.Get("content").String()
269234
270− // Create tool result message in Claude Code format
271− msg := map[string]interface{}{
272− "role": "user",
273− "content": []interface{}{
274− map[string]interface{}{
275− "type": "tool_result",
276− "tool_use_id": toolCallID,
277− "content": content,
278− },
279− },
280− }
281−
282− anthropicMessages = append(anthropicMessages, msg)
235+ msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
236+ msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
237+ msg, _ = sjson.Set(msg, "content.0.content", content)
238+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
283239 }
284240 return true
285241 })
286242 }
287243
288− // Set messages in the output template
289− if len(anthropicMessages) > 0 {
290− messagesJSON, _ := json.Marshal(anthropicMessages)
291− out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
292− }
293−
294244 // Tools mapping: OpenAI tools -> Claude Code tools
295245 if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
296− var anthropicTools []interface{}
246+ hasAnthropicTools := false
297247 tools.ForEach(func(_, tool gjson.Result) bool {
298248 if tool.Get("type").String() == "function" {
299249 function := tool.Get("function")
300− anthropicTool := map[string]interface{}{
301− "name": function.Get("name").String(),
302− "description": function.Get("description").String(),
303− }
250+ anthropicTool := `{"name":"","description":""}`
251+ anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String())
252+ anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String())
304253
305254 // Convert parameters schema for the tool
306255 if parameters := function.Get("parameters"); parameters.Exists() {
307− anthropicTool["input_schema"] = parameters.Value()
308− } else if parameters = function.Get("parametersJsonSchema"); parameters.Exists() {
309− anthropicTool["input_schema"] = parameters.Value()
256+ anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
257+ } else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() {
258+ anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
310259 }
311260
312− anthropicTools = append(anthropicTools, anthropicTool)
261+ out, _ = sjson.SetRaw(out, "tools.-1", anthropicTool)
262+ hasAnthropicTools = true
313263 }
314264 return true
315265 })
316266
317− if len(anthropicTools) > 0 {
318− toolsJSON, _ := json.Marshal(anthropicTools)
319− out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
267+ if !hasAnthropicTools {
268+ out, _ = sjson.Delete(out, "tools")
320269 }
321270 }
322271
@@ -329,18 +278,17 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines
329278 case "none":
330279 // Don't set tool_choice, Claude Code will not use tools
331280 case "auto":
332− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
281+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
333282 case "required":
334− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
283+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
335284 }
336285 case gjson.JSON:
337286 // Specific tool choice mapping
338287 if toolChoice.Get("type").String() == "function" {
339288 functionName := toolChoice.Get("function.name").String()
340− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{
341− "type": "tool",
342− "name": functionName,
343− })
289+ toolChoiceJSON := `{"type":"tool","name":""}`
290+ toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName)
291+ out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
344292 }
345293 default:
346294 }
internal/translator/claude/openai/chat-completions/claude_openai_response.go+46−62
@@ -8,7 +8,7 @@ package chat_completions · 7 unmodified lines
88 import (
99 "bytes"
1010 "context"
11− "encoding/json"
11+ "fmt"
1212 "strings"
1313 "time"
1414
@@ -178,18 +178,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 163 unmodified lines
178178 if arguments == "" {
179179 arguments = "{}"
180180 }
181−
182− toolCall := map[string]interface{}{
183− "index": index,
184− "id": accumulator.ID,
185− "type": "function",
186− "function": map[string]interface{}{
187− "name": accumulator.Name,
188− "arguments": arguments,
189− },
190− }
191−
192− template, _ = sjson.Set(template, "choices.0.delta.tool_calls", []interface{}{toolCall})
181+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index)
182+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID)
183+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function")
184+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name)
185+ template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.arguments", arguments)
193186
194187 // Clean up the accumulator for this index
195188 delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
@@ -210,12 +203,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 14 unmodified lines
210203
211204 // Handle usage information for token counts
212205 if usage := root.Get("usage"); usage.Exists() {
213− usageObj := map[string]interface{}{
214− "prompt_tokens": usage.Get("input_tokens").Int(),
215− "completion_tokens": usage.Get("output_tokens").Int(),
216− "total_tokens": usage.Get("input_tokens").Int() + usage.Get("output_tokens").Int(),
217− }
218− template, _ = sjson.Set(template, "usage", usageObj)
206+ inputTokens := usage.Get("input_tokens").Int()
207+ outputTokens := usage.Get("output_tokens").Int()
208+ template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens)
209+ template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens)
210+ template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens)
219211 }
220212 return []string{template}
221213
@@ -230,14 +222,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 8 unmodified lines
230222 case "error":
231223 // Error event - format and return error response
232224 if errorData := root.Get("error"); errorData.Exists() {
233− errorResponse := map[string]interface{}{
234− "error": map[string]interface{}{
235− "message": errorData.Get("message").String(),
236− "type": errorData.Get("type").String(),
237− },
238− }
239− errorJSON, _ := json.Marshal(errorResponse)
240− return []string{string(errorJSON)}
225+ errorJSON := `{"error":{"message":"","type":""}}`
226+ errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String())
227+ errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String())
228+ return []string{errorJSON}
241229 }
242230 return []string{}
243231
@@ -298,10 +286,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 54 unmodified lines
298286 var stopReason string
299287 var contentParts []string
300288 var reasoningParts []string
301− // Use map to track tool calls by index for proper merging
302− toolCallsMap := make(map[int]map[string]interface{})
303− // Track tool call arguments accumulation
304− toolCallArgsMap := make(map[int]strings.Builder)
289+ toolCallsAccumulator := make(map[int]*ToolCallAccumulator)
305290
306291 for _, chunk := range chunks {
307292 root := gjson.ParseBytes(chunk)
@@ -327,18 +312,12 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 19 unmodified lines
327312 // Start of thinking/reasoning content - skip for now as it's handled in delta
328313 continue
329314 } else if blockType == "tool_use" {
330− // Initialize tool call tracking for this index
315+ // Initialize tool call accumulator for this index
331316 index := int(root.Get("index").Int())
332− toolCallsMap[index] = map[string]interface{}{
333− "id": contentBlock.Get("id").String(),
334− "type": "function",
335− "function": map[string]interface{}{
336− "name": contentBlock.Get("name").String(),
337− "arguments": "",
338− },
317+ toolCallsAccumulator[index] = &ToolCallAccumulator{
318+ ID: contentBlock.Get("id").String(),
319+ Name: contentBlock.Get("name").String(),
339320 }
340− // Initialize arguments builder for this tool call
341− toolCallArgsMap[index] = strings.Builder{}
342321 }
343322 }
344323
@@ -361,9 +340,8 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 16 unmodified lines
361340 // Accumulate tool call arguments
362341 if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
363342 index := int(root.Get("index").Int())
364− if builder, exists := toolCallArgsMap[index]; exists {
365− builder.WriteString(partialJSON.String())
366− toolCallArgsMap[index] = builder
343+ if accumulator, exists := toolCallsAccumulator[index]; exists {
344+ accumulator.Arguments.WriteString(partialJSON.String())
367345 }
368346 }
369347 }
@@ -372,14 +350,9 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 2 unmodified lines
372350 case "content_block_stop":
373351 // Finalize tool call arguments for this index when content block ends
374352 index := int(root.Get("index").Int())
375− if toolCall, exists := toolCallsMap[index]; exists {
376− if builder, argsExists := toolCallArgsMap[index]; argsExists {
377− // Set the accumulated arguments for the tool call
378− arguments := builder.String()
379− if arguments == "" {
380− arguments = "{}"
381− }
382− toolCall["function"].(map[string]interface{})["arguments"] = arguments
353+ if accumulator, exists := toolCallsAccumulator[index]; exists {
354+ if accumulator.Arguments.Len() == 0 {
355+ accumulator.Arguments.WriteString("{}")
383356 }
384357 }
385358
@@ -417,24 +390,35 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 31 unmodified lines
417390 }
418391
419392 // Set tool calls if any were accumulated during processing
420− if len(toolCallsMap) > 0 {
421− // Convert tool calls map to array, preserving order by index
422− var toolCallsArray []interface{}
423− // Find the maximum index to determine the range
393+ if len(toolCallsAccumulator) > 0 {
394+ toolCallsCount := 0
424395 maxIndex := -1
425− for index := range toolCallsMap {
396+ for index := range toolCallsAccumulator {
426397 if index > maxIndex {
427398 maxIndex = index
428399 }
429400 }
430− // Iterate through all possible indices up to maxIndex
401+
431402 for i := 0; i <= maxIndex; i++ {
432− if toolCall, exists := toolCallsMap[i]; exists {
433− toolCallsArray = append(toolCallsArray, toolCall)
403+ accumulator, exists := toolCallsAccumulator[i]
404+ if !exists {
405+ continue
434406 }
407+
408+ arguments := accumulator.Arguments.String()
409+
410+ idPath := fmt.Sprintf("choices.0.message.tool_calls.%d.id", toolCallsCount)
411+ typePath := fmt.Sprintf("choices.0.message.tool_calls.%d.type", toolCallsCount)
412+ namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount)
413+ argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount)
414+
415+ out, _ = sjson.Set(out, idPath, accumulator.ID)
416+ out, _ = sjson.Set(out, typePath, "function")
417+ out, _ = sjson.Set(out, namePath, accumulator.Name)
418+ out, _ = sjson.Set(out, argumentsPath, arguments)
419+ toolCallsCount++
435420 }
436− if len(toolCallsArray) > 0 {
437− out, _ = sjson.Set(out, "choices.0.message.tool_calls", toolCallsArray)
421+ if toolCallsCount > 0 {
438422 out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
439423 } else {
440424 out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
internal/translator/claude/openai/responses/claude_openai-responses_request.go+9−4
@@ -254,7 +254,10 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte · 253 unmodified lines
254254 toolUse, _ = sjson.Set(toolUse, "id", callID)
255255 toolUse, _ = sjson.Set(toolUse, "name", name)
256256 if argsStr != "" && gjson.Valid(argsStr) {
257− toolUse, _ = sjson.SetRaw(toolUse, "input", argsStr)
257+ argsJSON := gjson.Parse(argsStr)
258+ if argsJSON.IsObject() {
259+ toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
260+ }
258261 }
259262
260263 asst := `{"role":"assistant","content":[]}`
@@ -309,16 +312,18 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte · 48 unmodified lines
309312 case gjson.String:
310313 switch toolChoice.String() {
311314 case "auto":
312− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})
315+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
313316 case "none":
314317 // Leave unset; implies no tools
315318 case "required":
316− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})
319+ out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
317320 }
318321 case gjson.JSON:
319322 if toolChoice.Get("type").String() == "function" {
320323 fn := toolChoice.Get("function.name").String()
321− out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "tool", "name": fn})
324+ toolChoiceJSON := `{"name":"","type":"tool"}`
325+ toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", fn)
326+ out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
322327 }
323328 default:
324329
internal/translator/claude/openai/responses/claude_openai-responses_response.go+34−59
@@ -344,31 +344,20 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin · 343 unmodified lines
344344 }
345345
346346 // Build response.output from aggregated state
347− var outputs []interface{}
347+ outputsWrapper := `{"arr":[]}`
348348 // reasoning item (if any)
349349 if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
350− r := map[string]interface{}{
351− "id": st.ReasoningItemID,
352− "type": "reasoning",
353− "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},
354− }
355− outputs = append(outputs, r)
350+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
351+ item, _ = sjson.Set(item, "id", st.ReasoningItemID)
352+ item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
353+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
356354 }
357355 // assistant message item (if any text)
358356 if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {
359− m := map[string]interface{}{
360− "id": st.CurrentMsgID,
361− "type": "message",
362− "status": "completed",
363− "content": []interface{}{map[string]interface{}{
364− "type": "output_text",
365− "annotations": []interface{}{},
366− "logprobs": []interface{}{},
367− "text": st.TextBuf.String(),
368− }},
369− "role": "assistant",
370− }
371− outputs = append(outputs, m)
357+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
358+ item, _ = sjson.Set(item, "id", st.CurrentMsgID)
359+ item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
360+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
372361 }
373362 // function_call items (in ascending index order for determinism)
374363 if len(st.FuncArgsBuf) > 0 {
@@ -395,19 +384,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin · 20 unmodified lines
395384 if callID == "" && st.CurrentFCID != "" {
396385 callID = st.CurrentFCID
397386 }
398− item := map[string]interface{}{
399− "id": fmt.Sprintf("fc_%s", callID),
400− "type": "function_call",
401− "status": "completed",
402− "arguments": args,
403− "call_id": callID,
404− "name": name,
405− }
406− outputs = append(outputs, item)
387+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
388+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
389+ item, _ = sjson.Set(item, "arguments", args)
390+ item, _ = sjson.Set(item, "call_id", callID)
391+ item, _ = sjson.Set(item, "name", name)
392+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
407393 }
408394 }
409− if len(outputs) > 0 {
410− completed, _ = sjson.Set(completed, "response.output", outputs)
395+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
396+ completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
411397 }
412398
413399 reasoningTokens := int64(0)
@@ -628,27 +614,18 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 214 unmodified lines
628614 }
629615
630616 // Build output array
631− var outputs []interface{}
617+ outputsWrapper := `{"arr":[]}`
632618 if reasoningBuf.Len() > 0 {
633− outputs = append(outputs, map[string]interface{}{
634− "id": reasoningItemID,
635− "type": "reasoning",
636− "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": reasoningBuf.String()}},
637− })
619+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
620+ item, _ = sjson.Set(item, "id", reasoningItemID)
621+ item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String())
622+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
638623 }
639624 if currentMsgID != "" || textBuf.Len() > 0 {
640− outputs = append(outputs, map[string]interface{}{
641− "id": currentMsgID,
642− "type": "message",
643− "status": "completed",
644− "content": []interface{}{map[string]interface{}{
645− "type": "output_text",
646− "annotations": []interface{}{},
647− "logprobs": []interface{}{},
648− "text": textBuf.String(),
649− }},
650− "role": "assistant",
651− })
625+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
626+ item, _ = sjson.Set(item, "id", currentMsgID)
627+ item, _ = sjson.Set(item, "content.0.text", textBuf.String())
628+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
652629 }
653630 if len(toolCalls) > 0 {
654631 // Preserve index order
@@ -669,18 +646,16 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 14 unmodified lines
669646 if args == "" {
670647 args = "{}"
671648 }
672− outputs = append(outputs, map[string]interface{}{
673− "id": fmt.Sprintf("fc_%s", st.id),
674− "type": "function_call",
675− "status": "completed",
676− "arguments": args,
677− "call_id": st.id,
678− "name": st.name,
679− })
649+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
650+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id))
651+ item, _ = sjson.Set(item, "arguments", args)
652+ item, _ = sjson.Set(item, "call_id", st.id)
653+ item, _ = sjson.Set(item, "name", st.name)
654+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
680655 }
681656 }
682− if len(outputs) > 0 {
683− out, _ = sjson.Set(out, "output", outputs)
657+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
658+ out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw)
684659 }
685660
686661 // Usage
internal/translator/codex/claude/codex_claude_response.go+31−57
@@ -9,7 +9,6 @@ package claude · 8 unmodified lines
99 import (
1010 "bytes"
1111 "context"
12− "encoding/json"
1312 "fmt"
1413 "strings"
1514
@@ -191,21 +190,12 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 175 unmodified lines
191190 return ""
192191 }
193192
194− response := map[string]interface{}{
195− "id": responseData.Get("id").String(),
196− "type": "message",
197− "role": "assistant",
198− "model": responseData.Get("model").String(),
199− "content": []interface{}{},
200− "stop_reason": nil,
201− "stop_sequence": nil,
202− "usage": map[string]interface{}{
203− "input_tokens": responseData.Get("usage.input_tokens").Int(),
204− "output_tokens": responseData.Get("usage.output_tokens").Int(),
205− },
206− }
193+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
194+ out, _ = sjson.Set(out, "id", responseData.Get("id").String())
195+ out, _ = sjson.Set(out, "model", responseData.Get("model").String())
196+ out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
197+ out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
207198
208− var contentBlocks []interface{}
209199 hasToolCall := false
210200
211201 if output := responseData.Get("output"); output.Exists() && output.IsArray() {
@@ -244,10 +234,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 32 unmodified lines
244234 }
245235 }
246236 if thinkingBuilder.Len() > 0 {
247− contentBlocks = append(contentBlocks, map[string]interface{}{
248− "type": "thinking",
249− "thinking": thinkingBuilder.String(),
250− })
237+ block := `{"type":"thinking","thinking":""}`
238+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
239+ out, _ = sjson.SetRaw(out, "content.-1", block)
251240 }
252241 case "message":
253242 if content := item.Get("content"); content.Exists() {
@@ -256,10 +245,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 2 unmodified lines
256245 if part.Get("type").String() == "output_text" {
257246 text := part.Get("text").String()
258247 if text != "" {
259− contentBlocks = append(contentBlocks, map[string]interface{}{
260− "type": "text",
261− "text": text,
262− })
248+ block := `{"type":"text","text":""}`
249+ block, _ = sjson.Set(block, "text", text)
250+ out, _ = sjson.SetRaw(out, "content.-1", block)
263251 }
264252 }
265253 return true
@@ -267,10 +255,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 1 unmodified line
267255 } else {
268256 text := content.String()
269257 if text != "" {
270− contentBlocks = append(contentBlocks, map[string]interface{}{
271− "type": "text",
272− "text": text,
273− })
258+ block := `{"type":"text","text":""}`
259+ block, _ = sjson.Set(block, "text", text)
260+ out, _ = sjson.SetRaw(out, "content.-1", block)
274261 }
275262 }
276263 }
@@ -281,54 +268,41 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 4 unmodified lines
281268 name = original
282269 }
283270
284− toolBlock := map[string]interface{}{
285− "type": "tool_use",
286− "id": item.Get("call_id").String(),
287− "name": name,
288− "input": map[string]interface{}{},
289− }
290−
291− if argsStr := item.Get("arguments").String(); argsStr != "" {
292− var args interface{}
293− if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
294− toolBlock["input"] = args
271+ toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
272+ toolBlock, _ = sjson.Set(toolBlock, "id", item.Get("call_id").String())
273+ toolBlock, _ = sjson.Set(toolBlock, "name", name)
274+ inputRaw := "{}"
275+ if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
276+ argsJSON := gjson.Parse(argsStr)
277+ if argsJSON.IsObject() {
278+ inputRaw = argsJSON.Raw
295279 }
296280 }
297−
298− contentBlocks = append(contentBlocks, toolBlock)
281+ toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
282+ out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
299283 }
300284 return true
301285 })
302286 }
303287
304− if len(contentBlocks) > 0 {
305− response["content"] = contentBlocks
306− }
307−
308288 if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
309− response["stop_reason"] = stopReason.String()
289+ out, _ = sjson.Set(out, "stop_reason", stopReason.String())
310290 } else if hasToolCall {
311− response["stop_reason"] = "tool_use"
291+ out, _ = sjson.Set(out, "stop_reason", "tool_use")
312292 } else {
313− response["stop_reason"] = "end_turn"
293+ out, _ = sjson.Set(out, "stop_reason", "end_turn")
314294 }
315295
316296 if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {
317− response["stop_sequence"] = stopSequence.Value()
297+ out, _ = sjson.SetRaw(out, "stop_sequence", stopSequence.Raw)
318298 }
319299
320300 if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
321− response["usage"] = map[string]interface{}{
322− "input_tokens": responseData.Get("usage.input_tokens").Int(),
323− "output_tokens": responseData.Get("usage.output_tokens").Int(),
324− }
301+ out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
302+ out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
325303 }
326304
327− responseJSON, err := json.Marshal(response)
328− if err != nil {
329− return ""
330− }
331− return string(responseJSON)
305+ return out
332306 }
333307
334308 // buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.
internal/translator/codex/gemini/codex_gemini_response.go+24−49
@@ -7,7 +7,6 @@ package gemini · 6 unmodified lines
77 import (
88 "bytes"
99 "context"
10− "encoding/json"
1110 "fmt"
1211 "time"
1312
@@ -190,19 +189,19 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 176 unmodified lines
190189 }
191190
192191 // Process output content to build parts array
193− var parts []interface{}
194192 hasToolCall := false
195− var pendingFunctionCalls []interface{}
193+ var pendingFunctionCalls []string
196194
197195 flushPendingFunctionCalls := func() {
198− if len(pendingFunctionCalls) > 0 {
199− // Add all pending function calls as individual parts
200− // This maintains the original Gemini API format while ensuring consecutive calls are grouped together
201− for _, fc := range pendingFunctionCalls {
202− parts = append(parts, fc)
203− }
204− pendingFunctionCalls = nil
196+ if len(pendingFunctionCalls) == 0 {
197+ return
198+ }
199+ // Add all pending function calls as individual parts
200+ // This maintains the original Gemini API format while ensuring consecutive calls are grouped together
201+ for _, fc := range pendingFunctionCalls {
202+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", fc)
205203 }
204+ pendingFunctionCalls = nil
206205 }
207206
208207 if output := responseData.Get("output"); output.Exists() && output.IsArray() {
@@ -216,11 +215,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 7 unmodified lines
216215
217216 // Add thinking content
218217 if content := value.Get("content"); content.Exists() {
219− part := map[string]interface{}{
220− "thought": true,
221− "text": content.String(),
222− }
223− parts = append(parts, part)
218+ part := `{"text":"","thought":true}`
219+ part, _ = sjson.Set(part, "text", content.String())
220+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
224221 }
225222
226223 case "message":
@@ -232,10 +229,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 5 unmodified lines
232229 content.ForEach(func(_, contentItem gjson.Result) bool {
233230 if contentItem.Get("type").String() == "output_text" {
234231 if text := contentItem.Get("text"); text.Exists() {
235− part := map[string]interface{}{
236− "text": text.String(),
237− }
238− parts = append(parts, part)
232+ part := `{"text":""}`
233+ part, _ = sjson.Set(part, "text", text.String())
234+ template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
239235 }
240236 }
241237 return true
@@ -245,28 +241,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 3 unmodified lines
245241 case "function_call":
246242 // Collect function call for potential merging with consecutive ones
247243 hasToolCall = true
248− functionCall := map[string]interface{}{
249− "functionCall": map[string]interface{}{
250− "name": func() string {
251− n := value.Get("name").String()
252− rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
253− if orig, ok := rev[n]; ok {
254− return orig
255− }
256− return n
257− }(),
258− "args": map[string]interface{}{},
259− },
244+ functionCall := `{"functionCall":{"args":{},"name":""}}`
245+ {
246+ n := value.Get("name").String()
247+ rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
248+ if orig, ok := rev[n]; ok {
249+ n = orig
250+ }
251+ functionCall, _ = sjson.Set(functionCall, "functionCall.name", n)
260252 }
261253
262254 // Parse and set arguments
263255 if argsStr := value.Get("arguments").String(); argsStr != "" {
264256 argsResult := gjson.Parse(argsStr)
265257 if argsResult.IsObject() {
266− var args map[string]interface{}
267− if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
268− functionCall["functionCall"].(map[string]interface{})["args"] = args
269− }
258+ functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr)
270259 }
271260 }
272261
@@ -279,11 +268,6 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 6 unmodified lines
279268 flushPendingFunctionCalls()
280269 }
281270
282− // Set the parts array
283− if len(parts) > 0 {
284− template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
285− }
286−
287271 // Set finish reason based on whether there were tool calls
288272 if hasToolCall {
289273 template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
@@ -323,15 +307,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { · 33 unmodified lines
323307 return rev
324308 }
325309
326−// mustMarshalJSON marshals a value to JSON, panicking on error.
327−func mustMarshalJSON(v interface{}) string {
328− data, err := json.Marshal(v)
329− if err != nil {
330− return ""
331− }
332− return string(data)
333−}
334−
335310 func GeminiTokenCount(ctx context.Context, count int64) string {
336311 return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
337312 }
internal/translator/gemini-cli/claude/gemini-cli_claude_request.go+78−83
@@ -7,10 +7,8 @@ package claude · 6 unmodified lines
77
88 import (
99 "bytes"
10− "encoding/json"
1110 "strings"
1211
13− client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
1412 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
1513 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
1614 "github.com/tidwall/gjson"
@@ -41,92 +39,100 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 24 unmodified lines
4139 rawJSON := bytes.Clone(inputRawJSON)
4240 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
4341
42+ // Build output Gemini CLI request JSON
43+ out := `{"model":"","request":{"contents":[]}}`
44+ out, _ = sjson.Set(out, "model", modelName)
45+
4446 // system instruction
45− var systemInstruction *client.Content
46− systemResult := gjson.GetBytes(rawJSON, "system")
47− if systemResult.IsArray() {
48− systemResults := systemResult.Array()
49− systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
50− for i := 0; i < len(systemResults); i++ {
51− systemPromptResult := systemResults[i]
52− systemTypePromptResult := systemPromptResult.Get("type")
53− if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
54− systemPrompt := systemPromptResult.Get("text").String()
55− systemPart := client.Part{Text: systemPrompt}
56− systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
47+ if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
48+ systemInstruction := `{"role":"user","parts":[]}`
49+ hasSystemParts := false
50+ systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
51+ if systemPromptResult.Get("type").String() == "text" {
52+ textResult := systemPromptResult.Get("text")
53+ if textResult.Type == gjson.String {
54+ part := `{"text":""}`
55+ part, _ = sjson.Set(part, "text", textResult.String())
56+ systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
57+ hasSystemParts = true
58+ }
5759 }
58− }
59− if len(systemInstruction.Parts) == 0 {
60− systemInstruction = nil
60+ return true
61+ })
62+ if hasSystemParts {
63+ out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction)
6164 }
6265 }
6366
6467 // contents
65− contents := make([]client.Content, 0)
66− messagesResult := gjson.GetBytes(rawJSON, "messages")
67− if messagesResult.IsArray() {
68− messageResults := messagesResult.Array()
69− for i := 0; i < len(messageResults); i++ {
70− messageResult := messageResults[i]
68+ if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
69+ messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
7170 roleResult := messageResult.Get("role")
7271 if roleResult.Type != gjson.String {
73− continue
72+ return true
7473 }
7574 role := roleResult.String()
7675 if role == "assistant" {
7776 role = "model"
7877 }
79− clientContent := client.Content{Role: role, Parts: []client.Part{}}
78+
79+ contentJSON := `{"role":"","parts":[]}`
80+ contentJSON, _ = sjson.Set(contentJSON, "role", role)
81+
8082 contentsResult := messageResult.Get("content")
8183 if contentsResult.IsArray() {
82− contentResults := contentsResult.Array()
83− for j := 0; j < len(contentResults); j++ {
84− contentResult := contentResults[j]
85− contentTypeResult := contentResult.Get("type")
86− if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
87− prompt := contentResult.Get("text").String()
88− clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
89− } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
84+ contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
85+ switch contentResult.Get("type").String() {
86+ case "text":
87+ part := `{"text":""}`
88+ part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
89+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
90+
91+ case "tool_use":
9092 functionName := contentResult.Get("name").String()
9193 functionArgs := contentResult.Get("input").String()
92− var args map[string]any
93− if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
94− clientContent.Parts = append(clientContent.Parts, client.Part{
95− FunctionCall: &client.FunctionCall{Name: functionName, Args: args},
96− ThoughtSignature: geminiCLIClaudeThoughtSignature,
97− })
94+ argsResult := gjson.Parse(functionArgs)
95+ if argsResult.IsObject() && gjson.Valid(functionArgs) {
96+ part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
97+ part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature)
98+ part, _ = sjson.Set(part, "functionCall.name", functionName)
99+ part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
100+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
98101 }
99− } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
102+
103+ case "tool_result":
100104 toolCallID := contentResult.Get("tool_use_id").String()
101− if toolCallID != "" {
102− funcName := toolCallID
103− toolCallIDs := strings.Split(toolCallID, "-")
104− if len(toolCallIDs) > 1 {
105− funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
106− }
107− responseData := contentResult.Get("content").Raw
108− functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
109− clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
105+ if toolCallID == "" {
106+ return true
107+ }
108+ funcName := toolCallID
109+ toolCallIDs := strings.Split(toolCallID, "-")
110+ if len(toolCallIDs) > 1 {
111+ funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
110112 }
113+ responseData := contentResult.Get("content").Raw
114+ part := `{"functionResponse":{"name":"","response":{"result":""}}}`
115+ part, _ = sjson.Set(part, "functionResponse.name", funcName)
116+ part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
117+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
111118 }
112− }
113− contents = append(contents, clientContent)
119+ return true
120+ })
121+ out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
114122 } else if contentsResult.Type == gjson.String {
115− prompt := contentsResult.String()
116− contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
123+ part := `{"text":""}`
124+ part, _ = sjson.Set(part, "text", contentsResult.String())
125+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
126+ out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
117127 }
118− }
128+ return true
129+ })
119130 }
120131
121132 // tools
122− var tools []client.ToolDeclaration
123− toolsResult := gjson.GetBytes(rawJSON, "tools")
124− if toolsResult.IsArray() {
125− tools = make([]client.ToolDeclaration, 1)
126− tools[0].FunctionDeclarations = make([]any, 0)
127− toolsResults := toolsResult.Array()
128− for i := 0; i < len(toolsResults); i++ {
129− toolResult := toolsResults[i]
133+ if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
134+ hasTools := false
135+ toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
130136 inputSchemaResult := toolResult.Get("input_schema")
131137 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
132138 inputSchema := inputSchemaResult.Raw
@@ -136,30 +142,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 3 unmodified lines
136142 tool, _ = sjson.Delete(tool, "input_examples")
137143 tool, _ = sjson.Delete(tool, "type")
138144 tool, _ = sjson.Delete(tool, "cache_control")
139− var toolDeclaration any
140− if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
141− tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
145+ if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
146+ if !hasTools {
147+ out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`)
148+ hasTools = true
149+ }
150+ out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool)
142151 }
143152 }
153+ return true
154+ })
155+ if !hasTools {
156+ out, _ = sjson.Delete(out, "request.tools")
144157 }
145− } else {
146− tools = make([]client.ToolDeclaration, 0)
147− }
148−
149− // Build output Gemini CLI request JSON
150− out := `{"model":"","request":{"contents":[]}}`
151− out, _ = sjson.Set(out, "model", modelName)
152− if systemInstruction != nil {
153− b, _ := json.Marshal(systemInstruction)
154− out, _ = sjson.SetRaw(out, "request.systemInstruction", string(b))
155− }
156− if len(contents) > 0 {
157− b, _ := json.Marshal(contents)
158− out, _ = sjson.SetRaw(out, "request.contents", string(b))
159− }
160− if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
161− b, _ := json.Marshal(tools)
162− out, _ = sjson.SetRaw(out, "request.tools", string(b))
163158 }
164159
165160 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled
internal/translator/gemini-cli/claude/gemini-cli_claude_response.go+26−49
@@ -9,7 +9,6 @@ package claude · 8 unmodified lines
99 import (
1010 "bytes"
1111 "context"
12− "encoding/json"
1312 "fmt"
1413 "strings"
1514 "sync/atomic"
@@ -276,22 +275,16 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 260 unmodified lines
276275
277276 root := gjson.ParseBytes(rawJSON)
278277
279− response := map[string]interface{}{
280− "id": root.Get("response.responseId").String(),
281− "type": "message",
282− "role": "assistant",
283− "model": root.Get("response.modelVersion").String(),
284− "content": []interface{}{},
285− "stop_reason": nil,
286− "stop_sequence": nil,
287− "usage": map[string]interface{}{
288− "input_tokens": root.Get("response.usageMetadata.promptTokenCount").Int(),
289− "output_tokens": root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int(),
290− },
291− }
278+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
279+ out, _ = sjson.Set(out, "id", root.Get("response.responseId").String())
280+ out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String())
281+
282+ inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
283+ outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int()
284+ out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
285+ out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
292286
293287 parts := root.Get("response.candidates.0.content.parts")
294− var contentBlocks []interface{}
295288 textBuilder := strings.Builder{}
296289 thinkingBuilder := strings.Builder{}
297290 toolIDCounter := 0
@@ -301,10 +294,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 3 unmodified lines
301294 if textBuilder.Len() == 0 {
302295 return
303296 }
304− contentBlocks = append(contentBlocks, map[string]interface{}{
305− "type": "text",
306− "text": textBuilder.String(),
307− })
297+ block := `{"type":"text","text":""}`
298+ block, _ = sjson.Set(block, "text", textBuilder.String())
299+ out, _ = sjson.SetRaw(out, "content.-1", block)
308300 textBuilder.Reset()
309301 }
310302
@@ -312,10 +304,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 1 unmodified line
312304 if thinkingBuilder.Len() == 0 {
313305 return
314306 }
315− contentBlocks = append(contentBlocks, map[string]interface{}{
316− "type": "thinking",
317− "thinking": thinkingBuilder.String(),
318− })
307+ block := `{"type":"thinking","thinking":""}`
308+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
309+ out, _ = sjson.SetRaw(out, "content.-1", block)
319310 thinkingBuilder.Reset()
320311 }
321312
@@ -339,21 +330,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 17 unmodified lines
339330
340331 name := functionCall.Get("name").String()
341332 toolIDCounter++
342− toolBlock := map[string]interface{}{
343− "type": "tool_use",
344− "id": fmt.Sprintf("tool_%d", toolIDCounter),
345− "name": name,
346− "input": map[string]interface{}{},
347− }
348−
349− if args := functionCall.Get("args"); args.Exists() {
350− var parsed interface{}
351− if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil {
352− toolBlock["input"] = parsed
353− }
333+ toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
334+ toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
335+ toolBlock, _ = sjson.Set(toolBlock, "name", name)
336+ inputRaw := "{}"
337+ if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
338+ inputRaw = args.Raw
354339 }
355−
356− contentBlocks = append(contentBlocks, toolBlock)
340+ toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
341+ out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
357342 continue
358343 }
359344 }
@@ -362,8 +347,6 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 2 unmodified lines
362347 flushThinking()
363348 flushText()
364349
365− response["content"] = contentBlocks
366−
367350 stopReason := "end_turn"
368351 if hasToolCall {
369352 stopReason = "tool_use"
@@ -379,19 +362,13 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 9 unmodified lines
379362 }
380363 }
381364 }
382− response["stop_reason"] = stopReason
365+ out, _ = sjson.Set(out, "stop_reason", stopReason)
383366
384− if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {
385− if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() {
386− delete(response, "usage")
387− }
367+ if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() {
368+ out, _ = sjson.Delete(out, "usage")
388369 }
389370
390− encoded, err := json.Marshal(response)
391− if err != nil {
392− return ""
393− }
394− return string(encoded)
371+ return out
395372 }
396373
397374 func ClaudeTokenCount(ctx context.Context, count int64) string {
internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go+27−51
@@ -7,7 +7,6 @@ package gemini · 6 unmodified lines
77
88 import (
99 "bytes"
10− "encoding/json"
1110 "fmt"
1211
1312 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
@@ -117,8 +116,6 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by · 103 unmodified lines
117116
118117 // FunctionCallGroup represents a group of function calls and their responses
119118 type FunctionCallGroup struct {
120− ModelContent map[string]interface{}
121− FunctionCalls []gjson.Result
122119 ResponsesNeeded int
123120 }
124121
@@ -146,7 +143,7 @@ func fixCLIToolResponse(input string) (string, error) { · 21 unmodified lines
146143 }
147144
148145 // Initialize data structures for processing and grouping
149− var newContents []interface{} // Final processed contents array
146+ contentsWrapper := `{"contents":[]}`
150147 var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
151148 var collectedResponses []gjson.Result // Standalone responses to be matched
152149
@@ -178,23 +175,17 @@ func fixCLIToolResponse(input string) (string, error) { · 25 unmodified lines
178175 collectedResponses = collectedResponses[group.ResponsesNeeded:]
179176
180177 // Create merged function response content
181− var responseParts []interface{}
178+ functionResponseContent := `{"parts":[],"role":"function"}`
182179 for _, response := range groupResponses {
183− var responseMap map[string]interface{}
184− errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)
185− if errUnmarshal != nil {
186− log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
180+ if !response.IsObject() {
181+ log.Warnf("failed to parse function response")
187182 continue
188183 }
189− responseParts = append(responseParts, responseMap)
184+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
190185 }
191186
192− if len(responseParts) > 0 {
193− functionResponseContent := map[string]interface{}{
194− "parts": responseParts,
195− "role": "function",
196− }
197− newContents = append(newContents, functionResponseContent)
187+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
188+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
198189 }
199190
200191 // Remove this group as it's been satisfied
@@ -208,50 +199,42 @@ func fixCLIToolResponse(input string) (string, error) { · 7 unmodified lines
208199
209200 // If this is a model with function calls, create a new group
210201 if role == "model" {
211− var functionCallsInThisModel []gjson.Result
202+ functionCallsCount := 0
212203 parts.ForEach(func(_, part gjson.Result) bool {
213204 if part.Get("functionCall").Exists() {
214− functionCallsInThisModel = append(functionCallsInThisModel, part)
205+ functionCallsCount++
215206 }
216207 return true
217208 })
218209
219− if len(functionCallsInThisModel) > 0 {
210+ if functionCallsCount > 0 {
220211 // Add the model content
221− var contentMap map[string]interface{}
222− errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
223− if errUnmarshal != nil {
224− log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
212+ if !value.IsObject() {
213+ log.Warnf("failed to parse model content")
225214 return true
226215 }
227− newContents = append(newContents, contentMap)
216+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
228217
229218 // Create a new group for tracking responses
230219 group := &FunctionCallGroup{
231− ModelContent: contentMap,
232− FunctionCalls: functionCallsInThisModel,
233− ResponsesNeeded: len(functionCallsInThisModel),
220+ ResponsesNeeded: functionCallsCount,
234221 }
235222 pendingGroups = append(pendingGroups, group)
236223 } else {
237224 // Regular model content without function calls
238− var contentMap map[string]interface{}
239− errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
240− if errUnmarshal != nil {
241− log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
225+ if !value.IsObject() {
226+ log.Warnf("failed to parse content")
242227 return true
243228 }
244− newContents = append(newContents, contentMap)
229+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
245230 }
246231 } else {
247232 // Non-model content (user, etc.)
248− var contentMap map[string]interface{}
249− errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)
250− if errUnmarshal != nil {
251− log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
233+ if !value.IsObject() {
234+ log.Warnf("failed to parse content")
252235 return true
253236 }
254− newContents = append(newContents, contentMap)
237+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
255238 }
256239
257240 return true
@@ -263,31 +246,24 @@ func fixCLIToolResponse(input string) (string, error) { · 5 unmodified lines
263246 groupResponses := collectedResponses[:group.ResponsesNeeded]
264247 collectedResponses = collectedResponses[group.ResponsesNeeded:]
265248
266− var responseParts []interface{}
249+ functionResponseContent := `{"parts":[],"role":"function"}`
267250 for _, response := range groupResponses {
268− var responseMap map[string]interface{}
269− errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)
270− if errUnmarshal != nil {
271− log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
251+ if !response.IsObject() {
252+ log.Warnf("failed to parse function response")
272253 continue
273254 }
274− responseParts = append(responseParts, responseMap)
255+ functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
275256 }
276257
277− if len(responseParts) > 0 {
278− functionResponseContent := map[string]interface{}{
279− "parts": responseParts,
280− "role": "function",
281− }
282− newContents = append(newContents, functionResponseContent)
258+ if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
259+ contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
283260 }
284261 }
285262 }
286263
287264 // Update the original JSON with the new contents
288265 result := input
289− newContentsJSON, _ := json.Marshal(newContents)
290− result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
266+ result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
291267
292268 return result, nil
293269 }
internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go+2−2
@@ -278,7 +278,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 277 unmodified lines
278278 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
279279 continue
280280 }
281− fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
281+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
282282 if errSet != nil {
283283 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
284284 continue
@@ -293,7 +293,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 8 unmodified lines
293293 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
294294 continue
295295 }
296− fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
296+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
297297 if errSet != nil {
298298 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
299299 continue
internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go+3−11
@@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines
88 import (
99 "bytes"
1010 "context"
11− "encoding/json"
1211 "fmt"
1312 "strings"
1413 "sync/atomic"
@@ -171,21 +170,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ · 156 unmodified lines
171170 mimeType = "image/png"
172171 }
173172 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
174− imagePayload, err := json.Marshal(map[string]any{
175− "type": "image_url",
176− "image_url": map[string]string{
177− "url": imageURL,
178− },
179− })
180− if err != nil {
181− continue
182− }
173+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
174+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
183175 imagesResult := gjson.Get(template, "choices.0.delta.images")
184176 if !imagesResult.Exists() || !imagesResult.IsArray() {
185177 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
186178 }
187179 template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
188− template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
180+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
189181 }
190182 }
191183 }
internal/translator/gemini/claude/gemini_claude_request.go+78−83
@@ -7,10 +7,8 @@ package claude · 6 unmodified lines
77
88 import (
99 "bytes"
10− "encoding/json"
1110 "strings"
1211
13− client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
1412 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
1513 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
1614 "github.com/tidwall/gjson"
@@ -34,92 +32,100 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 17 unmodified lines
3432 rawJSON := bytes.Clone(inputRawJSON)
3533 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
3634
35+ // Build output Gemini CLI request JSON
36+ out := `{"contents":[]}`
37+ out, _ = sjson.Set(out, "model", modelName)
38+
3739 // system instruction
38− var systemInstruction *client.Content
39− systemResult := gjson.GetBytes(rawJSON, "system")
40− if systemResult.IsArray() {
41− systemResults := systemResult.Array()
42− systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}
43− for i := 0; i < len(systemResults); i++ {
44− systemPromptResult := systemResults[i]
45− systemTypePromptResult := systemPromptResult.Get("type")
46− if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
47− systemPrompt := systemPromptResult.Get("text").String()
48− systemPart := client.Part{Text: systemPrompt}
49− systemInstruction.Parts = append(systemInstruction.Parts, systemPart)
40+ if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
41+ systemInstruction := `{"role":"user","parts":[]}`
42+ hasSystemParts := false
43+ systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
44+ if systemPromptResult.Get("type").String() == "text" {
45+ textResult := systemPromptResult.Get("text")
46+ if textResult.Type == gjson.String {
47+ part := `{"text":""}`
48+ part, _ = sjson.Set(part, "text", textResult.String())
49+ systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
50+ hasSystemParts = true
51+ }
5052 }
51− }
52− if len(systemInstruction.Parts) == 0 {
53− systemInstruction = nil
53+ return true
54+ })
55+ if hasSystemParts {
56+ out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction)
5457 }
5558 }
5659
5760 // contents
58− contents := make([]client.Content, 0)
59− messagesResult := gjson.GetBytes(rawJSON, "messages")
60− if messagesResult.IsArray() {
61− messageResults := messagesResult.Array()
62− for i := 0; i < len(messageResults); i++ {
63− messageResult := messageResults[i]
61+ if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
62+ messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
6463 roleResult := messageResult.Get("role")
6564 if roleResult.Type != gjson.String {
66− continue
65+ return true
6766 }
6867 role := roleResult.String()
6968 if role == "assistant" {
7069 role = "model"
7170 }
72− clientContent := client.Content{Role: role, Parts: []client.Part{}}
71+
72+ contentJSON := `{"role":"","parts":[]}`
73+ contentJSON, _ = sjson.Set(contentJSON, "role", role)
74+
7375 contentsResult := messageResult.Get("content")
7476 if contentsResult.IsArray() {
75− contentResults := contentsResult.Array()
76− for j := 0; j < len(contentResults); j++ {
77− contentResult := contentResults[j]
78− contentTypeResult := contentResult.Get("type")
79− if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
80− prompt := contentResult.Get("text").String()
81− clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})
82− } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
77+ contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
78+ switch contentResult.Get("type").String() {
79+ case "text":
80+ part := `{"text":""}`
81+ part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
82+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
83+
84+ case "tool_use":
8385 functionName := contentResult.Get("name").String()
8486 functionArgs := contentResult.Get("input").String()
85− var args map[string]any
86− if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {
87− clientContent.Parts = append(clientContent.Parts, client.Part{
88− FunctionCall: &client.FunctionCall{Name: functionName, Args: args},
89− ThoughtSignature: geminiClaudeThoughtSignature,
90− })
87+ argsResult := gjson.Parse(functionArgs)
88+ if argsResult.IsObject() && gjson.Valid(functionArgs) {
89+ part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
90+ part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature)
91+ part, _ = sjson.Set(part, "functionCall.name", functionName)
92+ part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
93+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
9194 }
92− } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
95+
96+ case "tool_result":
9397 toolCallID := contentResult.Get("tool_use_id").String()
94− if toolCallID != "" {
95− funcName := toolCallID
96− toolCallIDs := strings.Split(toolCallID, "-")
97− if len(toolCallIDs) > 1 {
98− funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
99− }
100− responseData := contentResult.Get("content").Raw
101− functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
102− clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
98+ if toolCallID == "" {
99+ return true
100+ }
101+ funcName := toolCallID
102+ toolCallIDs := strings.Split(toolCallID, "-")
103+ if len(toolCallIDs) > 1 {
104+ funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
103105 }
106+ responseData := contentResult.Get("content").Raw
107+ part := `{"functionResponse":{"name":"","response":{"result":""}}}`
108+ part, _ = sjson.Set(part, "functionResponse.name", funcName)
109+ part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
110+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
104111 }
105− }
106− contents = append(contents, clientContent)
112+ return true
113+ })
114+ out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
107115 } else if contentsResult.Type == gjson.String {
108− prompt := contentsResult.String()
109− contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})
116+ part := `{"text":""}`
117+ part, _ = sjson.Set(part, "text", contentsResult.String())
118+ contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
119+ out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
110120 }
111− }
121+ return true
122+ })
112123 }
113124
114125 // tools
115− var tools []client.ToolDeclaration
116− toolsResult := gjson.GetBytes(rawJSON, "tools")
117− if toolsResult.IsArray() {
118− tools = make([]client.ToolDeclaration, 1)
119− tools[0].FunctionDeclarations = make([]any, 0)
120− toolsResults := toolsResult.Array()
121− for i := 0; i < len(toolsResults); i++ {
122− toolResult := toolsResults[i]
126+ if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
127+ hasTools := false
128+ toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
123129 inputSchemaResult := toolResult.Get("input_schema")
124130 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
125131 inputSchema := inputSchemaResult.Raw
@@ -129,30 +135,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 3 unmodified lines
129135 tool, _ = sjson.Delete(tool, "input_examples")
130136 tool, _ = sjson.Delete(tool, "type")
131137 tool, _ = sjson.Delete(tool, "cache_control")
132− var toolDeclaration any
133− if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {
134− tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)
138+ if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
139+ if !hasTools {
140+ out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`)
141+ hasTools = true
142+ }
143+ out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool)
135144 }
136145 }
146+ return true
147+ })
148+ if !hasTools {
149+ out, _ = sjson.Delete(out, "tools")
137150 }
138− } else {
139− tools = make([]client.ToolDeclaration, 0)
140− }
141−
142− // Build output Gemini CLI request JSON
143− out := `{"contents":[]}`
144− out, _ = sjson.Set(out, "model", modelName)
145− if systemInstruction != nil {
146− b, _ := json.Marshal(systemInstruction)
147− out, _ = sjson.SetRaw(out, "system_instruction", string(b))
148− }
149− if len(contents) > 0 {
150− b, _ := json.Marshal(contents)
151− out, _ = sjson.SetRaw(out, "contents", string(b))
152− }
153− if len(tools) > 0 && len(tools[0].FunctionDeclarations) > 0 {
154− b, _ := json.Marshal(tools)
155− out, _ = sjson.SetRaw(out, "tools", string(b))
156151 }
157152
158153 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled
internal/translator/gemini/claude/gemini_claude_response.go+26−49
@@ -9,7 +9,6 @@ package claude · 8 unmodified lines
99 import (
1010 "bytes"
1111 "context"
12− "encoding/json"
1312 "fmt"
1413 "strings"
1514 "sync/atomic"
@@ -282,22 +281,16 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 266 unmodified lines
282281
283282 root := gjson.ParseBytes(rawJSON)
284283
285− response := map[string]interface{}{
286− "id": root.Get("responseId").String(),
287− "type": "message",
288− "role": "assistant",
289− "model": root.Get("modelVersion").String(),
290− "content": []interface{}{},
291− "stop_reason": nil,
292− "stop_sequence": nil,
293− "usage": map[string]interface{}{
294− "input_tokens": root.Get("usageMetadata.promptTokenCount").Int(),
295− "output_tokens": root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int(),
296− },
297− }
284+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
285+ out, _ = sjson.Set(out, "id", root.Get("responseId").String())
286+ out, _ = sjson.Set(out, "model", root.Get("modelVersion").String())
287+
288+ inputTokens := root.Get("usageMetadata.promptTokenCount").Int()
289+ outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int()
290+ out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
291+ out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
298292
299293 parts := root.Get("candidates.0.content.parts")
300− var contentBlocks []interface{}
301294 textBuilder := strings.Builder{}
302295 thinkingBuilder := strings.Builder{}
303296 toolIDCounter := 0
@@ -307,10 +300,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 3 unmodified lines
307300 if textBuilder.Len() == 0 {
308301 return
309302 }
310− contentBlocks = append(contentBlocks, map[string]interface{}{
311− "type": "text",
312− "text": textBuilder.String(),
313− })
303+ block := `{"type":"text","text":""}`
304+ block, _ = sjson.Set(block, "text", textBuilder.String())
305+ out, _ = sjson.SetRaw(out, "content.-1", block)
314306 textBuilder.Reset()
315307 }
316308
@@ -318,10 +310,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 1 unmodified line
318310 if thinkingBuilder.Len() == 0 {
319311 return
320312 }
321− contentBlocks = append(contentBlocks, map[string]interface{}{
322− "type": "thinking",
323− "thinking": thinkingBuilder.String(),
324− })
313+ block := `{"type":"thinking","thinking":""}`
314+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
315+ out, _ = sjson.SetRaw(out, "content.-1", block)
325316 thinkingBuilder.Reset()
326317 }
327318
@@ -345,21 +336,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 17 unmodified lines
345336
346337 name := functionCall.Get("name").String()
347338 toolIDCounter++
348− toolBlock := map[string]interface{}{
349− "type": "tool_use",
350− "id": fmt.Sprintf("tool_%d", toolIDCounter),
351− "name": name,
352− "input": map[string]interface{}{},
353− }
354−
355− if args := functionCall.Get("args"); args.Exists() {
356− var parsed interface{}
357− if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil {
358− toolBlock["input"] = parsed
359− }
339+ toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
340+ toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
341+ toolBlock, _ = sjson.Set(toolBlock, "name", name)
342+ inputRaw := "{}"
343+ if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
344+ inputRaw = args.Raw
360345 }
361−
362− contentBlocks = append(contentBlocks, toolBlock)
346+ toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
347+ out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
363348 continue
364349 }
365350 }
@@ -368,8 +353,6 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 2 unmodified lines
368353 flushThinking()
369354 flushText()
370355
371− response["content"] = contentBlocks
372−
373356 stopReason := "end_turn"
374357 if hasToolCall {
375358 stopReason = "tool_use"
@@ -385,19 +368,13 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 9 unmodified lines
385368 }
386369 }
387370 }
388− response["stop_reason"] = stopReason
371+ out, _ = sjson.Set(out, "stop_reason", stopReason)
389372
390− if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {
391− if usageMeta := root.Get("usageMetadata"); !usageMeta.Exists() {
392− delete(response, "usage")
393− }
373+ if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() {
374+ out, _ = sjson.Delete(out, "usage")
394375 }
395376
396− encoded, err := json.Marshal(response)
397− if err != nil {
398− return ""
399− }
400− return string(encoded)
377+ return out
401378 }
402379
403380 func ClaudeTokenCount(ctx context.Context, count int64) string {
internal/translator/gemini/openai/chat-completions/gemini_openai_request.go+2−2
@@ -320,7 +320,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 319 unmodified lines
320320 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
321321 continue
322322 }
323− fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
323+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
324324 if errSet != nil {
325325 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
326326 continue
@@ -335,7 +335,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 8 unmodified lines
335335 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
336336 continue
337337 }
338− fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})
338+ fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
339339 if errSet != nil {
340340 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
341341 continue
internal/translator/gemini/openai/chat-completions/gemini_openai_response.go+6−21
@@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines
88 import (
99 "bytes"
1010 "context"
11− "encoding/json"
1211 "fmt"
1312 "strings"
1413 "sync/atomic"
@@ -173,21 +172,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR · 158 unmodified lines
173172 mimeType = "image/png"
174173 }
175174 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
176− imagePayload, err := json.Marshal(map[string]any{
177− "type": "image_url",
178− "image_url": map[string]string{
179− "url": imageURL,
180− },
181− })
182− if err != nil {
183− continue
184− }
175+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
176+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
185177 imagesResult := gjson.Get(template, "choices.0.delta.images")
186178 if !imagesResult.Exists() || !imagesResult.IsArray() {
187179 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
188180 }
189181 template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
190− template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", string(imagePayload))
182+ template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
191183 }
192184 }
193185 }
@@ -305,21 +297,14 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina · 111 unmodified lines
305297 mimeType = "image/png"
306298 }
307299 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
308− imagePayload, err := json.Marshal(map[string]any{
309− "type": "image_url",
310− "image_url": map[string]string{
311− "url": imageURL,
312− },
313− })
314− if err != nil {
315− continue
316− }
300+ imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
301+ imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
317302 imagesResult := gjson.Get(template, "choices.0.message.images")
318303 if !imagesResult.Exists() || !imagesResult.IsArray() {
319304 template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)
320305 }
321306 template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
322− template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", string(imagePayload))
307+ template, _ = sjson.SetRaw(template, "choices.0.message.images.-1", imagePayload)
323308 }
324309 }
325310 }
internal/translator/gemini/openai/responses/gemini_openai-responses_response.go+53−72
@@ -377,27 +377,18 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, · 376 unmodified lines
377377 }
378378
379379 // Compose outputs in encountered order: reasoning, message, function_calls
380− var outputs []interface{}
380+ outputsWrapper := `{"arr":[]}`
381381 if st.ReasoningOpened {
382− outputs = append(outputs, map[string]interface{}{
383− "id": st.ReasoningItemID,
384− "type": "reasoning",
385− "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},
386− })
382+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
383+ item, _ = sjson.Set(item, "id", st.ReasoningItemID)
384+ item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
385+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
387386 }
388387 if st.MsgOpened {
389− outputs = append(outputs, map[string]interface{}{
390− "id": st.CurrentMsgID,
391− "type": "message",
392− "status": "completed",
393− "content": []interface{}{map[string]interface{}{
394− "type": "output_text",
395− "annotations": []interface{}{},
396− "logprobs": []interface{}{},
397− "text": st.TextBuf.String(),
398− }},
399− "role": "assistant",
400− })
388+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
389+ item, _ = sjson.Set(item, "id", st.CurrentMsgID)
390+ item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
391+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
401392 }
402393 if len(st.FuncArgsBuf) > 0 {
403394 idxs := make([]int, 0, len(st.FuncArgsBuf))
@@ -416,18 +407,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, · 12 unmodified lines
416407 if b := st.FuncArgsBuf[idx]; b != nil {
417408 args = b.String()
418409 }
419− outputs = append(outputs, map[string]interface{}{
420− "id": fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]),
421− "type": "function_call",
422− "status": "completed",
423− "arguments": args,
424− "call_id": st.FuncCallIDs[idx],
425− "name": st.FuncNames[idx],
426− })
410+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
411+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
412+ item, _ = sjson.Set(item, "arguments", args)
413+ item, _ = sjson.Set(item, "call_id", st.FuncCallIDs[idx])
414+ item, _ = sjson.Set(item, "name", st.FuncNames[idx])
415+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
427416 }
428417 }
429− if len(outputs) > 0 {
430− completed, _ = sjson.Set(completed, "response.output", outputs)
418+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
419+ completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
431420 }
432421
433422 // usage mapping
@@ -558,11 +547,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 124 unmodified lines
558547 }
559548
560549 // Build outputs from candidates[0].content.parts
561− var outputs []interface{}
562550 var reasoningText strings.Builder
563551 var reasoningEncrypted string
564552 var messageText strings.Builder
565553 var haveMessage bool
554+
555+ haveOutput := false
556+ ensureOutput := func() {
557+ if haveOutput {
558+ return
559+ }
560+ resp, _ = sjson.SetRaw(resp, "output", "[]")
561+ haveOutput = true
562+ }
563+ appendOutput := func(itemJSON string) {
564+ ensureOutput()
565+ resp, _ = sjson.SetRaw(resp, "output.-1", itemJSON)
566+ }
567+
566568 if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() {
567569 parts.ForEach(func(_, p gjson.Result) bool {
568570 if p.Get("thought").Bool() {
@@ -583,19 +585,16 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 14 unmodified lines
583585 name := fc.Get("name").String()
584586 args := fc.Get("args")
585587 callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
586− outputs = append(outputs, map[string]interface{}{
587− "id": fmt.Sprintf("fc_%s", callID),
588− "type": "function_call",
589− "status": "completed",
590− "arguments": func() string {
591− if args.Exists() {
592− return args.Raw
593− }
594− return ""
595− }(),
596− "call_id": callID,
597− "name": name,
598− })
588+ itemJSON := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
589+ itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID))
590+ itemJSON, _ = sjson.Set(itemJSON, "call_id", callID)
591+ itemJSON, _ = sjson.Set(itemJSON, "name", name)
592+ argsStr := ""
593+ if args.Exists() {
594+ argsStr = args.Raw
595+ }
596+ itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr)
597+ appendOutput(itemJSON)
599598 return true
600599 }
601600 return true
@@ -605,42 +604,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 3 unmodified lines
605604 // Reasoning output item
606605 if reasoningText.Len() > 0 || reasoningEncrypted != "" {
607606 rid := strings.TrimPrefix(id, "resp_")
608− item := map[string]interface{}{
609− "id": fmt.Sprintf("rs_%s", rid),
610− "type": "reasoning",
611− "encrypted_content": reasoningEncrypted,
612− }
613− var summaries []interface{}
607+ itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}`
608+ itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid))
609+ itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted)
614610 if reasoningText.Len() > 0 {
615− summaries = append(summaries, map[string]interface{}{
616− "type": "summary_text",
617− "text": reasoningText.String(),
618− })
619− }
620− if summaries != nil {
621− item["summary"] = summaries
611+ summaryJSON := `{"type":"summary_text","text":""}`
612+ summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String())
613+ itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]")
614+ itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON)
622615 }
623− outputs = append(outputs, item)
616+ appendOutput(itemJSON)
624617 }
625618
626619 // Assistant message output item
627620 if haveMessage {
628− outputs = append(outputs, map[string]interface{}{
629− "id": fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")),
630− "type": "message",
631− "status": "completed",
632− "content": []interface{}{map[string]interface{}{
633− "type": "output_text",
634− "annotations": []interface{}{},
635− "logprobs": []interface{}{},
636− "text": messageText.String(),
637− }},
638− "role": "assistant",
639− })
640− }
641−
642− if len(outputs) > 0 {
643− resp, _ = sjson.Set(resp, "output", outputs)
621+ itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
622+ itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")))
623+ itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String())
624+ appendOutput(itemJSON)
644625 }
645626
646627 // usage mapping
internal/translator/openai/claude/openai_claude_request.go+2−8
@@ -7,7 +7,6 @@ package claude · 6 unmodified lines
77
88 import (
99 "bytes"
10− "encoding/json"
1110 "strings"
1211
1312 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
@@ -138,11 +137,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 124 unmodified lines
138137
139138 // Convert input to arguments JSON string
140139 if input := part.Get("input"); input.Exists() {
141− if inputJSON, err := json.Marshal(input.Value()); err == nil {
142− toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", string(inputJSON))
143− } else {
144− toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
145− }
140+ toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw)
146141 } else {
147142 toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
148143 }
@@ -191,8 +186,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 42 unmodified lines
191186 // Emit tool calls in a separate assistant message
192187 if role == "assistant" && len(toolCalls) > 0 {
193188 toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`
194− toolCallsJSON, _ := json.Marshal(toolCalls)
195− toolCallMsgJSON, _ = sjson.SetRaw(toolCallMsgJSON, "tool_calls", string(toolCallsJSON))
189+ toolCallMsgJSON, _ = sjson.Set(toolCallMsgJSON, "tool_calls", toolCalls)
196190 messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())
197191 }
198192
internal/translator/openai/claude/openai_claude_response.go+136−307
@@ -8,7 +8,6 @@ package claude · 7 unmodified lines
88 import (
99 "bytes"
1010 "context"
11− "encoding/json"
1211 "fmt"
1312 "strings"
1413
@@ -133,24 +132,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 118 unmodified lines
133132 if delta := root.Get("choices.0.delta"); delta.Exists() {
134133 if !param.MessageStarted {
135134 // Send message_start event
136− messageStart := map[string]interface{}{
137− "type": "message_start",
138− "message": map[string]interface{}{
139− "id": param.MessageID,
140− "type": "message",
141− "role": "assistant",
142− "model": param.Model,
143− "content": []interface{}{},
144− "stop_reason": nil,
145− "stop_sequence": nil,
146− "usage": map[string]interface{}{
147− "input_tokens": 0,
148− "output_tokens": 0,
149− },
150− },
151− }
152− messageStartJSON, _ := json.Marshal(messageStart)
153− results = append(results, "event: message_start\ndata: "+string(messageStartJSON)+"\n\n")
135+ 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}}}`
136+ messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID)
137+ messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model)
138+ results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n")
154139 param.MessageStarted = true
155140
156141 // Don't send content_block_start for text here - wait for actual content
@@ -168,29 +153,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 11 unmodified lines
168153 param.ThinkingContentBlockIndex = param.NextContentBlockIndex
169154 param.NextContentBlockIndex++
170155 }
171− contentBlockStart := map[string]interface{}{
172− "type": "content_block_start",
173− "index": param.ThinkingContentBlockIndex,
174− "content_block": map[string]interface{}{
175− "type": "thinking",
176− "thinking": "",
177− },
178− }
179− contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
180− results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
156+ contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
157+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex)
158+ results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
181159 param.ThinkingContentBlockStarted = true
182160 }
183161
184− thinkingDelta := map[string]interface{}{
185− "type": "content_block_delta",
186− "index": param.ThinkingContentBlockIndex,
187− "delta": map[string]interface{}{
188− "type": "thinking_delta",
189− "thinking": reasoningText,
190− },
191− }
192− thinkingDeltaJSON, _ := json.Marshal(thinkingDelta)
193− results = append(results, "event: content_block_delta\ndata: "+string(thinkingDeltaJSON)+"\n\n")
162+ thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
163+ thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex)
164+ thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText)
165+ results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n")
194166 }
195167 }
196168
@@ -203,29 +175,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 6 unmodified lines
203175 param.TextContentBlockIndex = param.NextContentBlockIndex
204176 param.NextContentBlockIndex++
205177 }
206− contentBlockStart := map[string]interface{}{
207− "type": "content_block_start",
208− "index": param.TextContentBlockIndex,
209− "content_block": map[string]interface{}{
210− "type": "text",
211− "text": "",
212− },
213− }
214− contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
215− results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
178+ contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
179+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex)
180+ results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
216181 param.TextContentBlockStarted = true
217182 }
218183
219− contentDelta := map[string]interface{}{
220− "type": "content_block_delta",
221− "index": param.TextContentBlockIndex,
222− "delta": map[string]interface{}{
223− "type": "text_delta",
224− "text": content.String(),
225− },
226− }
227− contentDeltaJSON, _ := json.Marshal(contentDelta)
228− results = append(results, "event: content_block_delta\ndata: "+string(contentDeltaJSON)+"\n\n")
184+ contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
185+ contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex)
186+ contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String())
187+ results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n")
229188
230189 // Accumulate content
231190 param.ContentAccumulator.WriteString(content.String())
@@ -263,18 +222,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 31 unmodified lines
263222 stopTextContentBlock(param, &results)
264223
265224 // Send content_block_start for tool_use
266− contentBlockStart := map[string]interface{}{
267− "type": "content_block_start",
268− "index": blockIndex,
269− "content_block": map[string]interface{}{
270− "type": "tool_use",
271− "id": accumulator.ID,
272− "name": accumulator.Name,
273− "input": map[string]interface{}{},
274− },
275− }
276− contentBlockStartJSON, _ := json.Marshal(contentBlockStart)
277− results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n")
225+ contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
226+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
227+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", accumulator.ID)
228+ contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
229+ results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
278230 }
279231
280232 // Handle function arguments
@@ -298,12 +250,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 17 unmodified lines
298250
299251 // Send content_block_stop for thinking content if needed
300252 if param.ThinkingContentBlockStarted {
301− contentBlockStop := map[string]interface{}{
302− "type": "content_block_stop",
303− "index": param.ThinkingContentBlockIndex,
304− }
305− contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
306− results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
253+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
254+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
255+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
307256 param.ThinkingContentBlockStarted = false
308257 param.ThinkingContentBlockIndex = -1
309258 }
@@ -319,24 +268,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 9 unmodified lines
319268
320269 // Send complete input_json_delta with all accumulated arguments
321270 if accumulator.Arguments.Len() > 0 {
322− inputDelta := map[string]interface{}{
323− "type": "content_block_delta",
324− "index": blockIndex,
325− "delta": map[string]interface{}{
326− "type": "input_json_delta",
327− "partial_json": util.FixJSON(accumulator.Arguments.String()),
328− },
329− }
330− inputDeltaJSON, _ := json.Marshal(inputDelta)
331− results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
271+ inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
272+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
273+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
274+ results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
332275 }
333276
334− contentBlockStop := map[string]interface{}{
335− "type": "content_block_stop",
336− "index": blockIndex,
337− }
338− contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
339− results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
277+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
278+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
279+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
340280 delete(param.ToolCallBlockIndexes, index)
341281 }
342282 param.ContentBlocksStopped = true
@@ -361,20 +301,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 18 unmodified lines
361301 }
362302 }
363303 // Send message_delta with usage
364− messageDelta := map[string]interface{}{
365− "type": "message_delta",
366− "delta": map[string]interface{}{
367− "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
368− "stop_sequence": nil,
369− },
370− "usage": map[string]interface{}{
371− "input_tokens": inputTokens,
372− "output_tokens": outputTokens,
373− },
374− }
375−
376− messageDeltaJSON, _ := json.Marshal(messageDelta)
377− results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
304+ messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
305+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
306+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
307+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
308+ results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
378309 param.MessageDeltaSent = true
379310
380311 emitMessageStopIfNeeded(param, &results)
@@ -390,12 +321,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 9 unmodified lines
390321
391322 // Ensure all content blocks are stopped before final events
392323 if param.ThinkingContentBlockStarted {
393− contentBlockStop := map[string]interface{}{
394− "type": "content_block_stop",
395− "index": param.ThinkingContentBlockIndex,
396− }
397− contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
398− results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
324+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
325+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
326+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
399327 param.ThinkingContentBlockStarted = false
400328 param.ThinkingContentBlockIndex = -1
401329 }
@@ -408,24 +336,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 6 unmodified lines
408336 blockIndex := param.toolContentBlockIndex(index)
409337
410338 if accumulator.Arguments.Len() > 0 {
411− inputDelta := map[string]interface{}{
412− "type": "content_block_delta",
413− "index": blockIndex,
414− "delta": map[string]interface{}{
415− "type": "input_json_delta",
416− "partial_json": util.FixJSON(accumulator.Arguments.String()),
417− },
418− }
419− inputDeltaJSON, _ := json.Marshal(inputDelta)
420− results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n")
339+ inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
340+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
341+ inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
342+ results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
421343 }
422344
423− contentBlockStop := map[string]interface{}{
424− "type": "content_block_stop",
425− "index": blockIndex,
426− }
427− contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
428− results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
345+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
346+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
347+ results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
429348 delete(param.ToolCallBlockIndexes, index)
430349 }
431350 param.ContentBlocksStopped = true
@@ -433,16 +352,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 1 unmodified line
433352
434353 // If we haven't sent message_delta yet (no usage info was received), send it now
435354 if param.FinishReason != "" && !param.MessageDeltaSent {
436− messageDelta := map[string]interface{}{
437− "type": "message_delta",
438− "delta": map[string]interface{}{
439− "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),
440− "stop_sequence": nil,
441− },
442− }
443−
444− messageDeltaJSON, _ := json.Marshal(messageDelta)
445− results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n")
355+ messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null}}`
356+ messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
357+ results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
446358 param.MessageDeltaSent = true
447359 }
448360
@@ -455,105 +367,73 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 6 unmodified lines
455367 func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
456368 root := gjson.ParseBytes(rawJSON)
457369
458− // Build Anthropic response
459− response := map[string]interface{}{
460− "id": root.Get("id").String(),
461− "type": "message",
462− "role": "assistant",
463− "model": root.Get("model").String(),
464− "content": []interface{}{},
465− "stop_reason": nil,
466− "stop_sequence": nil,
467− "usage": map[string]interface{}{
468− "input_tokens": 0,
469− "output_tokens": 0,
470− },
471− }
370+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
371+ out, _ = sjson.Set(out, "id", root.Get("id").String())
372+ out, _ = sjson.Set(out, "model", root.Get("model").String())
472373
473374 // Process message content and tool calls
474− var contentBlocks []interface{}
475−
476− if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
375+ if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
477376 choice := choices.Array()[0] // Take first choice
478− reasoningNode := choice.Get("message.reasoning_content")
479− allReasoning := collectOpenAIReasoningTexts(reasoningNode)
480377
481− for _, reasoningText := range allReasoning {
378+ reasoningNode := choice.Get("message.reasoning_content")
379+ for _, reasoningText := range collectOpenAIReasoningTexts(reasoningNode) {
482380 if reasoningText == "" {
483381 continue
484382 }
485− contentBlocks = append(contentBlocks, map[string]interface{}{
486− "type": "thinking",
487− "thinking": reasoningText,
488− })
383+ block := `{"type":"thinking","thinking":""}`
384+ block, _ = sjson.Set(block, "thinking", reasoningText)
385+ out, _ = sjson.SetRaw(out, "content.-1", block)
489386 }
490387
491388 // Handle text content
492389 if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
493− textBlock := map[string]interface{}{
494− "type": "text",
495− "text": content.String(),
496− }
497− contentBlocks = append(contentBlocks, textBlock)
390+ block := `{"type":"text","text":""}`
391+ block, _ = sjson.Set(block, "text", content.String())
392+ out, _ = sjson.SetRaw(out, "content.-1", block)
498393 }
499394
500395 // Handle tool calls
501396 if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
502397 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
503− toolUseBlock := map[string]interface{}{
504− "type": "tool_use",
505− "id": toolCall.Get("id").String(),
506− "name": toolCall.Get("function.name").String(),
507− }
508−
509− // Parse arguments
510− argsStr := toolCall.Get("function.arguments").String()
511− argsStr = util.FixJSON(argsStr)
512− if argsStr != "" {
513− var args interface{}
514− if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
515− toolUseBlock["input"] = args
398+ toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
399+ toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
400+ toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
401+
402+ argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
403+ if argsStr != "" && gjson.Valid(argsStr) {
404+ argsJSON := gjson.Parse(argsStr)
405+ if argsJSON.IsObject() {
406+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
516407 } else {
517− toolUseBlock["input"] = map[string]interface{}{}
408+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
518409 }
519410 } else {
520− toolUseBlock["input"] = map[string]interface{}{}
411+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
521412 }
522413
523− contentBlocks = append(contentBlocks, toolUseBlock)
414+ out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
524415 return true
525416 })
526417 }
527418
528419 // Set stop reason
529420 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
530− response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
421+ out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
531422 }
532423 }
533424
534− response["content"] = contentBlocks
535−
536425 // Set usage information
537426 if usage := root.Get("usage"); usage.Exists() {
538− response["usage"] = map[string]interface{}{
539− "input_tokens": usage.Get("prompt_tokens").Int(),
540− "output_tokens": usage.Get("completion_tokens").Int(),
541− "reasoning_tokens": func() int64 {
542− if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
543− return v.Int()
544− }
545− return 0
546− }(),
547− }
548− } else {
549− response["usage"] = map[string]interface{}{
550− "input_tokens": 0,
551− "output_tokens": 0,
427+ out, _ = sjson.Set(out, "usage.input_tokens", usage.Get("prompt_tokens").Int())
428+ out, _ = sjson.Set(out, "usage.output_tokens", usage.Get("completion_tokens").Int())
429+ reasoningTokens := int64(0)
430+ if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
431+ reasoningTokens = v.Int()
552432 }
433+ out, _ = sjson.Set(out, "usage.reasoning_tokens", reasoningTokens)
553434 }
554435
555− responseJSON, _ := json.Marshal(response)
556− return []string{string(responseJSON)}
436+ return []string{out}
557437 }
558438
559439 // mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
@@ -620,12 +500,9 @@ func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, res · 60 unmodified lines
620500 if !param.ThinkingContentBlockStarted {
621501 return
622502 }
623− contentBlockStop := map[string]interface{}{
624− "type": "content_block_stop",
625− "index": param.ThinkingContentBlockIndex,
626− }
627− contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
628− *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
503+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
504+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
505+ *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
629506 param.ThinkingContentBlockStarted = false
630507 param.ThinkingContentBlockIndex = -1
631508 }
@@ -642,12 +519,9 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results · 10 unmodified lines
642519 if !param.TextContentBlockStarted {
643520 return
644521 }
645− contentBlockStop := map[string]interface{}{
646− "type": "content_block_stop",
647− "index": param.TextContentBlockIndex,
648− }
649− contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
650− *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
522+ contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
523+ contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex)
524+ *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
651525 param.TextContentBlockStarted = false
652526 param.TextContentBlockIndex = -1
653527 }
@@ -667,29 +541,19 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 13 unmodified lines
667541 _ = requestRawJSON
668542
669543 root := gjson.ParseBytes(rawJSON)
544+ out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
545+ out, _ = sjson.Set(out, "id", root.Get("id").String())
546+ out, _ = sjson.Set(out, "model", root.Get("model").String())
670547
671− response := map[string]interface{}{
672− "id": root.Get("id").String(),
673− "type": "message",
674− "role": "assistant",
675− "model": root.Get("model").String(),
676− "content": []interface{}{},
677− "stop_reason": nil,
678− "stop_sequence": nil,
679− "usage": map[string]interface{}{
680− "input_tokens": 0,
681− "output_tokens": 0,
682− },
683− }
684−
685− contentBlocks := make([]interface{}, 0)
686548 hasToolCall := false
549+ stopReasonSet := false
687550
688551 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
689552 choice := choices.Array()[0]
690553
691554 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
692− response["stop_reason"] = mapOpenAIFinishReasonToAnthropic(finishReason.String())
555+ out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String()))
556+ stopReasonSet = true
693557 }
694558
695559 if message := choice.Get("message"); message.Exists() {
@@ -702,10 +566,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 6 unmodified lines
702566 if textBuilder.Len() == 0 {
703567 return
704568 }
705− contentBlocks = append(contentBlocks, map[string]interface{}{
706− "type": "text",
707− "text": textBuilder.String(),
708− })
569+ block := `{"type":"text","text":""}`
570+ block, _ = sjson.Set(block, "text", textBuilder.String())
571+ out, _ = sjson.SetRaw(out, "content.-1", block)
709572 textBuilder.Reset()
710573 }
711574
@@ -713,16 +576,14 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 1 unmodified line
713576 if thinkingBuilder.Len() == 0 {
714577 return
715578 }
716− contentBlocks = append(contentBlocks, map[string]interface{}{
717− "type": "thinking",
718− "thinking": thinkingBuilder.String(),
719− })
579+ block := `{"type":"thinking","thinking":""}`
580+ block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
581+ out, _ = sjson.SetRaw(out, "content.-1", block)
720582 thinkingBuilder.Reset()
721583 }
722584
723585 for _, item := range contentResult.Array() {
724− typeStr := item.Get("type").String()
725− switch typeStr {
586+ switch item.Get("type").String() {
726587 case "text":
727588 flushThinking()
728589 textBuilder.WriteString(item.Get("text").String())
@@ -733,25 +594,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 4 unmodified lines
733594 if toolCalls.IsArray() {
734595 toolCalls.ForEach(func(_, tc gjson.Result) bool {
735596 hasToolCall = true
736− toolUse := map[string]interface{}{
737− "type": "tool_use",
738− "id": tc.Get("id").String(),
739− "name": tc.Get("function.name").String(),
740− }
597+ toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
598+ toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String())
599+ toolUse, _ = sjson.Set(toolUse, "name", tc.Get("function.name").String())
741600
742601 argsStr := util.FixJSON(tc.Get("function.arguments").String())
743− if argsStr != "" {
744− var parsed interface{}
745− if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil {
746− toolUse["input"] = parsed
602+ if argsStr != "" && gjson.Valid(argsStr) {
603+ argsJSON := gjson.Parse(argsStr)
604+ if argsJSON.IsObject() {
605+ toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
747606 } else {
748− toolUse["input"] = map[string]interface{}{}
607+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
749608 }
750609 } else {
751− toolUse["input"] = map[string]interface{}{}
610+ toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
752611 }
753612
754− contentBlocks = append(contentBlocks, toolUse)
613+ out, _ = sjson.SetRaw(out, "content.-1", toolUse)
755614 return true
756615 })
757616 }
@@ -771,10 +630,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 13 unmodified lines
771630 } else if contentResult.Type == gjson.String {
772631 textContent := contentResult.String()
773632 if textContent != "" {
774− contentBlocks = append(contentBlocks, map[string]interface{}{
775− "type": "text",
776− "text": textContent,
777− })
633+ block := `{"type":"text","text":""}`
634+ block, _ = sjson.Set(block, "text", textContent)
635+ out, _ = sjson.SetRaw(out, "content.-1", block)
778636 }
779637 }
780638 }
@@ -784,81 +642,52 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 3 unmodified lines
784642 if reasoningText == "" {
785643 continue
786644 }
787− contentBlocks = append(contentBlocks, map[string]interface{}{
788− "type": "thinking",
789− "thinking": reasoningText,
790− })
645+ block := `{"type":"thinking","thinking":""}`
646+ block, _ = sjson.Set(block, "thinking", reasoningText)
647+ out, _ = sjson.SetRaw(out, "content.-1", block)
791648 }
792649 }
793650
794651 if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
795652 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
796653 hasToolCall = true
797− toolUseBlock := map[string]interface{}{
798− "type": "tool_use",
799− "id": toolCall.Get("id").String(),
800− "name": toolCall.Get("function.name").String(),
801− }
802−
803− argsStr := toolCall.Get("function.arguments").String()
804− argsStr = util.FixJSON(argsStr)
805− if argsStr != "" {
806− var args interface{}
807− if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
808− toolUseBlock["input"] = args
654+ toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
655+ toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
656+ toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
657+
658+ argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
659+ if argsStr != "" && gjson.Valid(argsStr) {
660+ argsJSON := gjson.Parse(argsStr)
661+ if argsJSON.IsObject() {
662+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
809663 } else {
810− toolUseBlock["input"] = map[string]interface{}{}
664+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
811665 }
812666 } else {
813− toolUseBlock["input"] = map[string]interface{}{}
667+ toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
814668 }
815669
816− contentBlocks = append(contentBlocks, toolUseBlock)
670+ out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
817671 return true
818672 })
819673 }
820674 }
821675 }
822676
823− response["content"] = contentBlocks
824−
825677 if respUsage := root.Get("usage"); respUsage.Exists() {
826− usageJSON := `{}`
827− usageJSON, _ = sjson.Set(usageJSON, "input_tokens", respUsage.Get("prompt_tokens").Int())
828− usageJSON, _ = sjson.Set(usageJSON, "output_tokens", respUsage.Get("completion_tokens").Int())
829− parsedUsage := gjson.Parse(usageJSON).Value().(map[string]interface{})
830− response["usage"] = parsedUsage
831− } else {
832− response["usage"] = `{"input_tokens":0,"output_tokens":0}`
678+ out, _ = sjson.Set(out, "usage.input_tokens", respUsage.Get("prompt_tokens").Int())
679+ out, _ = sjson.Set(out, "usage.output_tokens", respUsage.Get("completion_tokens").Int())
833680 }
834681
835− if response["stop_reason"] == nil {
682+ if !stopReasonSet {
836683 if hasToolCall {
837− response["stop_reason"] = "tool_use"
684+ out, _ = sjson.Set(out, "stop_reason", "tool_use")
838685 } else {
839− response["stop_reason"] = "end_turn"
686+ out, _ = sjson.Set(out, "stop_reason", "end_turn")
840687 }
841688 }
842689
843− if !hasToolCall {
844− if toolBlocks := response["content"].([]interface{}); len(toolBlocks) > 0 {
845− for _, block := range toolBlocks {
846− if m, ok := block.(map[string]interface{}); ok && m["type"] == "tool_use" {
847− hasToolCall = true
848− break
849− }
850− }
851− }
852− if hasToolCall {
853− response["stop_reason"] = "tool_use"
854− }
855− }
856−
857− responseJSON, err := json.Marshal(response)
858− if err != nil {
859− return ""
860− }
861− return string(responseJSON)
690+ return out
862691 }
863692
864693 func ClaudeTokenCount(ctx context.Context, count int64) string {
internal/translator/openai/gemini/openai_gemini_request.go+53−202
@@ -8,7 +8,6 @@ package gemini · 7 unmodified lines
88 import (
99 "bytes"
1010 "crypto/rand"
11− "encoding/json"
1211 "fmt"
1312 "math/big"
1413 "strings"
@@ -94,7 +93,6 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 79 unmodified lines
9493 out, _ = sjson.Set(out, "stream", stream)
9594
9695 // Process contents (Gemini messages) -> OpenAI messages
97− var openAIMessages []interface{}
9896 var toolCallIDs []string // Track tool call IDs for matching with tool results
9997
10098 // System instruction -> OpenAI system message
@@ -105,22 +103,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 4 unmodified lines
105103 }
106104 if systemInstruction.Exists() {
107105 parts := systemInstruction.Get("parts")
108− msg := map[string]interface{}{
109− "role": "system",
110− "content": []interface{}{},
111− }
112−
113− var aggregatedParts []interface{}
106+ msg := `{"role":"system","content":[]}`
107+ hasContent := false
114108
115109 if parts.Exists() && parts.IsArray() {
116110 parts.ForEach(func(_, part gjson.Result) bool {
117111 // Handle text parts
118112 if text := part.Get("text"); text.Exists() {
119− formattedText := text.String()
120− aggregatedParts = append(aggregatedParts, map[string]interface{}{
121− "type": "text",
122− "text": formattedText,
123− })
113+ contentPart := `{"type":"text","text":""}`
114+ contentPart, _ = sjson.Set(contentPart, "text", text.String())
115+ msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
116+ hasContent = true
124117 }
125118
126119 // Handle inline data (e.g., images)
@@ -132,20 +125,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 5 unmodified lines
132125 data := inlineData.Get("data").String()
133126 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
134127
135− aggregatedParts = append(aggregatedParts, map[string]interface{}{
136− "type": "image_url",
137− "image_url": map[string]interface{}{
138− "url": imageURL,
139− },
140− })
128+ contentPart := `{"type":"image_url","image_url":{"url":""}}`
129+ contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
130+ msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
131+ hasContent = true
141132 }
142133 return true
143134 })
144135 }
145136
146− if len(aggregatedParts) > 0 {
147− msg["content"] = aggregatedParts
148− openAIMessages = append(openAIMessages, msg)
137+ if hasContent {
138+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
149139 }
150140 }
151141
@@ -159,16 +149,15 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 7 unmodified lines
159149 role = "assistant"
160150 }
161151
162− // Create OpenAI message
163− msg := map[string]interface{}{
164− "role": role,
165− "content": "",
166− }
152+ msg := `{"role":"","content":""}`
153+ msg, _ = sjson.Set(msg, "role", role)
167154
168155 var textBuilder strings.Builder
169− var aggregatedParts []interface{}
156+ contentWrapper := `{"arr":[]}`
157+ contentPartsCount := 0
170158 onlyTextContent := true
171− var toolCalls []interface{}
159+ toolCallsWrapper := `{"arr":[]}`
160+ toolCallsCount := 0
172161
173162 if parts.Exists() && parts.IsArray() {
174163 parts.ForEach(func(_, part gjson.Result) bool {
@@ -176,10 +165,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line
176165 if text := part.Get("text"); text.Exists() {
177166 formattedText := text.String()
178167 textBuilder.WriteString(formattedText)
179− aggregatedParts = append(aggregatedParts, map[string]interface{}{
180− "type": "text",
181− "text": formattedText,
182− })
168+ contentPart := `{"type":"text","text":""}`
169+ contentPart, _ = sjson.Set(contentPart, "text", formattedText)
170+ contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
171+ contentPartsCount++
183172 }
184173
185174 // Handle inline data (e.g., images)
@@ -193,12 +182,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 7 unmodified lines
193182 data := inlineData.Get("data").String()
194183 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
195184
196− aggregatedParts = append(aggregatedParts, map[string]interface{}{
197− "type": "image_url",
198− "image_url": map[string]interface{}{
199− "url": imageURL,
200− },
201− })
185+ contentPart := `{"type":"image_url","image_url":{"url":""}}`
186+ contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
187+ contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
188+ contentPartsCount++
202189 }
203190
204191 // Handle function calls (Gemini) -> tool calls (OpenAI)
@@ -206,44 +193,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line
206193 toolCallID := genToolCallID()
207194 toolCallIDs = append(toolCallIDs, toolCallID)
208195
209− toolCall := map[string]interface{}{
210− "id": toolCallID,
211− "type": "function",
212− "function": map[string]interface{}{
213− "name": functionCall.Get("name").String(),
214− },
215− }
196+ toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
197+ toolCall, _ = sjson.Set(toolCall, "id", toolCallID)
198+ toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String())
216199
217200 // Convert args to arguments JSON string
218201 if args := functionCall.Get("args"); args.Exists() {
219− argsJSON, _ := json.Marshal(args.Value())
220− toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
202+ toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw)
221203 } else {
222− toolCall["function"].(map[string]interface{})["arguments"] = "{}"
204+ toolCall, _ = sjson.Set(toolCall, "function.arguments", "{}")
223205 }
224206
225− toolCalls = append(toolCalls, toolCall)
207+ toolCallsWrapper, _ = sjson.SetRaw(toolCallsWrapper, "arr.-1", toolCall)
208+ toolCallsCount++
226209 }
227210
228211 // Handle function responses (Gemini) -> tool role messages (OpenAI)
229212 if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
230213 // Create tool message for function response
231− toolMsg := map[string]interface{}{
232− "role": "tool",
233− "tool_call_id": "", // Will be set based on context
234− "content": "",
235− }
214+ toolMsg := `{"role":"tool","tool_call_id":"","content":""}`
236215
237216 // Convert response.content to JSON string
238217 if response := functionResponse.Get("response"); response.Exists() {
239− if content = response.Get("content"); content.Exists() {
240− // Use the content field from the response
241− contentJSON, _ := json.Marshal(content.Value())
242− toolMsg["content"] = string(contentJSON)
218+ if contentField := response.Get("content"); contentField.Exists() {
219+ toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw)
243220 } else {
244− // Fallback to entire response
245− responseJSON, _ := json.Marshal(response.Value())
246− toolMsg["content"] = string(responseJSON)
221+ toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw)
247222 }
248223 }
249224
@@ -252,13 +227,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 2 unmodified lines
252227 if len(toolCallIDs) > 0 {
253228 // Use the last tool call ID (simple matching by function name)
254229 // In a real implementation, you might want more sophisticated matching
255− toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
230+ toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1])
256231 } else {
257232 // Generate a tool call ID if none available
258− toolMsg["tool_call_id"] = genToolCallID()
233+ toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", genToolCallID())
259234 }
260235
261− openAIMessages = append(openAIMessages, toolMsg)
236+ out, _ = sjson.SetRaw(out, "messages.-1", toolMsg)
262237 }
263238
264239 return true
@@ -266,170 +241,46 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line
266241 }
267242
268243 // Set content
269− if len(aggregatedParts) > 0 {
244+ if contentPartsCount > 0 {
270245 if onlyTextContent {
271− msg["content"] = textBuilder.String()
246+ msg, _ = sjson.Set(msg, "content", textBuilder.String())
272247 } else {
273− msg["content"] = aggregatedParts
248+ msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw)
274249 }
275250 }
276251
277252 // Set tool calls if any
278− if len(toolCalls) > 0 {
279− msg["tool_calls"] = toolCalls
253+ if toolCallsCount > 0 {
254+ msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw)
280255 }
281256
282− openAIMessages = append(openAIMessages, msg)
283−
284− // switch role {
285− // case "user", "model":
286− // // Convert role: model -> assistant
287− // if role == "model" {
288− // role = "assistant"
289− // }
290− //
291− // // Create OpenAI message
292− // msg := map[string]interface{}{
293− // "role": role,
294− // "content": "",
295− // }
296− //
297− // var contentParts []string
298− // var toolCalls []interface{}
299− //
300− // if parts.Exists() && parts.IsArray() {
301− // parts.ForEach(func(_, part gjson.Result) bool {
302− // // Handle text parts
303− // if text := part.Get("text"); text.Exists() {
304− // contentParts = append(contentParts, text.String())
305− // }
306− //
307− // // Handle function calls (Gemini) -> tool calls (OpenAI)
308− // if functionCall := part.Get("functionCall"); functionCall.Exists() {
309− // toolCallID := genToolCallID()
310− // toolCallIDs = append(toolCallIDs, toolCallID)
311− //
312− // toolCall := map[string]interface{}{
313− // "id": toolCallID,
314− // "type": "function",
315− // "function": map[string]interface{}{
316− // "name": functionCall.Get("name").String(),
317− // },
318− // }
319− //
320− // // Convert args to arguments JSON string
321− // if args := functionCall.Get("args"); args.Exists() {
322− // argsJSON, _ := json.Marshal(args.Value())
323− // toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
324− // } else {
325− // toolCall["function"].(map[string]interface{})["arguments"] = "{}"
326− // }
327− //
328− // toolCalls = append(toolCalls, toolCall)
329− // }
330− //
331− // return true
332− // })
333− // }
334− //
335− // // Set content
336− // if len(contentParts) > 0 {
337− // msg["content"] = strings.Join(contentParts, "")
338− // }
339− //
340− // // Set tool calls if any
341− // if len(toolCalls) > 0 {
342− // msg["tool_calls"] = toolCalls
343− // }
344− //
345− // openAIMessages = append(openAIMessages, msg)
346− //
347− // case "function":
348− // // Handle Gemini function role -> OpenAI tool role
349− // if parts.Exists() && parts.IsArray() {
350− // parts.ForEach(func(_, part gjson.Result) bool {
351− // // Handle function responses (Gemini) -> tool role messages (OpenAI)
352− // if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
353− // // Create tool message for function response
354− // toolMsg := map[string]interface{}{
355− // "role": "tool",
356− // "tool_call_id": "", // Will be set based on context
357− // "content": "",
358− // }
359− //
360− // // Convert response.content to JSON string
361− // if response := functionResponse.Get("response"); response.Exists() {
362− // if content = response.Get("content"); content.Exists() {
363− // // Use the content field from the response
364− // contentJSON, _ := json.Marshal(content.Value())
365− // toolMsg["content"] = string(contentJSON)
366− // } else {
367− // // Fallback to entire response
368− // responseJSON, _ := json.Marshal(response.Value())
369− // toolMsg["content"] = string(responseJSON)
370− // }
371− // }
372− //
373− // // Try to match with previous tool call ID
374− // _ = functionResponse.Get("name").String() // functionName not used for now
375− // if len(toolCallIDs) > 0 {
376− // // Use the last tool call ID (simple matching by function name)
377− // // In a real implementation, you might want more sophisticated matching
378− // toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1]
379− // } else {
380− // // Generate a tool call ID if none available
381− // toolMsg["tool_call_id"] = genToolCallID()
382− // }
383− //
384− // openAIMessages = append(openAIMessages, toolMsg)
385− // }
386− //
387− // return true
388− // })
389− // }
390− // }
257+ out, _ = sjson.SetRaw(out, "messages.-1", msg)
391258 return true
392259 })
393260 }
394261
395− // Set messages
396− if len(openAIMessages) > 0 {
397− messagesJSON, _ := json.Marshal(openAIMessages)
398− out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
399− }
400−
401262 // Tools mapping: Gemini tools -> OpenAI tools
402263 if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
403− var openAITools []interface{}
404264 tools.ForEach(func(_, tool gjson.Result) bool {
405265 if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
406266 functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
407− openAITool := map[string]interface{}{
408− "type": "function",
409− "function": map[string]interface{}{
410− "name": funcDecl.Get("name").String(),
411− "description": funcDecl.Get("description").String(),
412− },
413− }
267+ openAITool := `{"type":"function","function":{"name":"","description":""}}`
268+ openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String())
269+ openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String())
414270
415271 // Convert parameters schema
416272 if parameters := funcDecl.Get("parameters"); parameters.Exists() {
417− openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
418− } else if parameters = funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
419− openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()
273+ openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
274+ } else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
275+ openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
420276 }
421277
422− openAITools = append(openAITools, openAITool)
278+ out, _ = sjson.SetRaw(out, "tools.-1", openAITool)
423279 return true
424280 })
425281 }
426282 return true
427283 })
428−
429− if len(openAITools) > 0 {
430− toolsJSON, _ := json.Marshal(openAITools)
431− out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
432− }
433284 }
434285
435286 // Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it)
internal/translator/openai/gemini/openai_gemini_response.go+68−107
@@ -8,7 +8,6 @@ package gemini · 7 unmodified lines
88 import (
99 "bytes"
1010 "context"
11− "encoding/json"
1211 "fmt"
1312 "strconv"
1413 "strings"
@@ -84,15 +83,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 69 unmodified lines
8483 template, _ = sjson.Set(template, "model", model.String())
8584 }
8685
87− usageObj := map[string]interface{}{
88− "promptTokenCount": usage.Get("prompt_tokens").Int(),
89− "candidatesTokenCount": usage.Get("completion_tokens").Int(),
90− "totalTokenCount": usage.Get("total_tokens").Int(),
91− }
86+ template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
87+ template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
88+ template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
9289 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
93− usageObj["thoughtsTokenCount"] = reasoningTokens
90+ template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
9491 }
95− template, _ = sjson.Set(template, "usageMetadata", usageObj)
9692 return []string{template}
9793 }
9894 return []string{}
@@ -133,13 +129,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 34 unmodified lines
133129 continue
134130 }
135131 reasoningTemplate := baseTemplate
136− parts := []interface{}{
137− map[string]interface{}{
138− "thought": true,
139− "text": reasoningText,
140− },
141− }
142− reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts", parts)
132+ reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
133+ reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
143134 chunkOutputs = append(chunkOutputs, reasoningTemplate)
144135 }
145136 }
@@ -150,13 +141,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 4 unmodified lines
150141 (*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
151142
152143 // Create text part for this delta
153− parts := []interface{}{
154− map[string]interface{}{
155− "text": contentText,
156− },
157− }
158144 contentTemplate := baseTemplate
159− contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts", parts)
145+ contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
160146 chunkOutputs = append(chunkOutputs, contentTemplate)
161147 }
162148
@@ -225,24 +211,13 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 62 unmodified lines
225211
226212 // If we have accumulated tool calls, output them now
227213 if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
228− var parts []interface{}
214+ partIndex := 0
229215 for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
230− argsStr := accumulator.Arguments.String()
231− var argsMap map[string]interface{}
232−
233− argsMap = parseArgsToMap(argsStr)
234−
235− functionCallPart := map[string]interface{}{
236− "functionCall": map[string]interface{}{
237− "name": accumulator.Name,
238− "args": argsMap,
239− },
240− }
241− parts = append(parts, functionCallPart)
242− }
243−
244− if len(parts) > 0 {
245− template, _ = sjson.Set(template, "candidates.0.content.parts", parts)
216+ namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
217+ argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
218+ template, _ = sjson.Set(template, namePath, accumulator.Name)
219+ template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
220+ partIndex++
246221 }
247222
248223 // Clear accumulators
@@ -255,15 +230,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 6 unmodified lines
255230
256231 // Handle usage information
257232 if usage := root.Get("usage"); usage.Exists() {
258− usageObj := map[string]interface{}{
259− "promptTokenCount": usage.Get("prompt_tokens").Int(),
260− "candidatesTokenCount": usage.Get("completion_tokens").Int(),
261− "totalTokenCount": usage.Get("total_tokens").Int(),
262− }
233+ template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
234+ template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
235+ template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
263236 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
264− usageObj["thoughtsTokenCount"] = reasoningTokens
237+ template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
265238 }
266− template, _ = sjson.Set(template, "usageMetadata", usageObj)
267239 results = append(results, template)
268240 return true
269241 }
@@ -291,46 +263,54 @@ func mapOpenAIFinishReasonToGemini(openAIReason string) string { · 21 unmodified lines
291263 }
292264 }
293265
294−// parseArgsToMap safely parses a JSON string of function arguments into a map.
295−// It returns an empty map if the input is empty or cannot be parsed as a JSON object.
296−func parseArgsToMap(argsStr string) map[string]interface{} {
266+// parseArgsToObjectRaw safely parses a JSON string of function arguments into an object JSON string.
267+// It returns "{}" if the input is empty or cannot be parsed as a JSON object.
268+func parseArgsToObjectRaw(argsStr string) string {
297269 trimmed := strings.TrimSpace(argsStr)
298270 if trimmed == "" || trimmed == "{}" {
299− return map[string]interface{}{}
271+ return "{}"
300272 }
301273
302274 // First try strict JSON
303− var out map[string]interface{}
304− if errUnmarshal := json.Unmarshal([]byte(trimmed), &out); errUnmarshal == nil {
305− return out
275+ if gjson.Valid(trimmed) {
276+ strict := gjson.Parse(trimmed)
277+ if strict.IsObject() {
278+ return strict.Raw
279+ }
306280 }
307281
308282 // Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)
309− tolerant := tolerantParseJSONMap(trimmed)
310− if len(tolerant) > 0 {
283+ tolerant := tolerantParseJSONObjectRaw(trimmed)
284+ if tolerant != "{}" {
311285 return tolerant
312286 }
313287
314288 // Fallback: return empty object when parsing fails
315− return map[string]interface{}{}
289+ return "{}"
316290 }
317291
318−// tolerantParseJSONMap attempts to parse a JSON-like object string into a map, tolerating
292+func escapeSjsonPathKey(key string) string {
293+ key = strings.ReplaceAll(key, `\`, `\\`)
294+ key = strings.ReplaceAll(key, `.`, `\.`)
295+ return key
296+}
297+
298+// tolerantParseJSONObjectRaw attempts to parse a JSON-like object string into a JSON object string, tolerating
319299 // bareword values (unquoted strings) commonly seen during streamed tool calls.
320300 // Example input: {"location": 北京, "unit": celsius}
321−func tolerantParseJSONMap(s string) map[string]interface{} {
301+func tolerantParseJSONObjectRaw(s string) string {
322302 // Ensure we operate within the outermost braces if present
323303 start := strings.Index(s, "{")
324304 end := strings.LastIndex(s, "}")
325305 if start == -1 || end == -1 || start >= end {
326− return map[string]interface{}{}
306+ return "{}"
327307 }
328308 content := s[start+1 : end]
329309
330310 runes := []rune(content)
331311 n := len(runes)
332312 i := 0
333− result := make(map[string]interface{})
313+ result := "{}"
334314
335315 for i < n {
336316 // Skip whitespace and commas
@@ -356,6 +336,7 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 19 unmodified lines
356336 break
357337 }
358338 keyName := jsonStringTokenToRawString(keyToken)
339+ sjsonKey := escapeSjsonPathKey(keyName)
359340 i = nextIdx
360341
361342 // Skip whitespace
@@ -375,17 +356,16 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 13 unmodified lines
375356 }
376357
377358 // Parse value (string, number, object/array, bareword)
378− var value interface{}
379359 switch runes[i] {
380360 case '"':
381361 // JSON string
382362 valToken, ni := parseJSONStringRunes(runes, i)
383363 if ni == -1 {
384364 // Malformed; treat as empty string
385− value = ""
365+ result, _ = sjson.Set(result, sjsonKey, "")
386366 i = n
387367 } else {
388− value = jsonStringTokenToRawString(valToken)
368+ result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
389369 i = ni
390370 }
391371 case '{', '[':
@@ -394,11 +374,10 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 2 unmodified lines
394374 if ni == -1 {
395375 i = n
396376 } else {
397− var anyVal interface{}
398− if errUnmarshal := json.Unmarshal([]byte(seg), &anyVal); errUnmarshal == nil {
399− value = anyVal
377+ if gjson.Valid(seg) {
378+ result, _ = sjson.SetRaw(result, sjsonKey, seg)
400379 } else {
401− value = seg
380+ result, _ = sjson.Set(result, sjsonKey, seg)
402381 }
403382 i = ni
404383 }
@@ -411,21 +390,19 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 6 unmodified lines
411390 token := strings.TrimSpace(string(runes[i:j]))
412391 // Interpret common JSON atoms and numbers; otherwise treat as string
413392 if token == "true" {
414− value = true
393+ result, _ = sjson.Set(result, sjsonKey, true)
415394 } else if token == "false" {
416− value = false
395+ result, _ = sjson.Set(result, sjsonKey, false)
417396 } else if token == "null" {
418− value = nil
397+ result, _ = sjson.Set(result, sjsonKey, nil)
419398 } else if numVal, ok := tryParseNumber(token); ok {
420− value = numVal
399+ result, _ = sjson.Set(result, sjsonKey, numVal)
421400 } else {
422− value = token
401+ result, _ = sjson.Set(result, sjsonKey, token)
423402 }
424403 i = j
425404 }
426405
427− result[keyName] = value
428−
429406 // Skip trailing whitespace and optional comma before next pair
430407 for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {
431408 i++
@@ -463,9 +440,9 @@ func parseJSONStringRunes(runes []rune, start int) (string, int) { · 31 unmodified lines
463440
464441 // jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.
465442 func jsonStringTokenToRawString(token string) string {
466− var s string
467− if errUnmarshal := json.Unmarshal([]byte(token), &s); errUnmarshal == nil {
468− return s
443+ r := gjson.Parse(token)
444+ if r.Type == gjson.String {
445+ return r.String()
469446 }
470447 // Fallback: strip surrounding quotes if present
471448 if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {
@@ -579,7 +556,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 107 unmodified lines
579556 }
580557 }
581558
582− var parts []interface{}
559+ partIndex := 0
583560
584561 // Handle reasoning content before visible text
585562 if reasoning := message.Get("reasoning_content"); reasoning.Exists() {
@@ -587,18 +564,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 1 unmodified line
587564 if reasoningText == "" {
588565 continue
589566 }
590− parts = append(parts, map[string]interface{}{
591− "thought": true,
592− "text": reasoningText,
593− })
567+ out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
568+ out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
569+ partIndex++
594570 }
595571 }
596572
597573 // Handle content first
598574 if content := message.Get("content"); content.Exists() && content.String() != "" {
599− parts = append(parts, map[string]interface{}{
600− "text": content.String(),
601− })
575+ out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
576+ partIndex++
602577 }
603578
604579 // Handle tool calls
@@ -609,27 +584,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 4 unmodified lines
609584 functionName := function.Get("name").String()
610585 functionArgs := function.Get("arguments").String()
611586
612− // Parse arguments
613− var argsMap map[string]interface{}
614− argsMap = parseArgsToMap(functionArgs)
615−
616− functionCallPart := map[string]interface{}{
617− "functionCall": map[string]interface{}{
618− "name": functionName,
619− "args": argsMap,
620− },
621− }
622− parts = append(parts, functionCallPart)
587+ namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
588+ argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
589+ out, _ = sjson.Set(out, namePath, functionName)
590+ out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
591+ partIndex++
623592 }
624593 return true
625594 })
626595 }
627596
628− // Set parts
629− if len(parts) > 0 {
630− out, _ = sjson.Set(out, "candidates.0.content.parts", parts)
631− }
632−
633597 // Handle finish reason
634598 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
635599 geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
@@ -645,15 +609,12 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 9 unmodified lines
645609
646610 // Handle usage information
647611 if usage := root.Get("usage"); usage.Exists() {
648− usageObj := map[string]interface{}{
649− "promptTokenCount": usage.Get("prompt_tokens").Int(),
650− "candidatesTokenCount": usage.Get("completion_tokens").Int(),
651− "totalTokenCount": usage.Get("total_tokens").Int(),
652− }
612+ out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
613+ out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
614+ out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
653615 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
654− usageObj["thoughtsTokenCount"] = reasoningTokens
616+ out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
655617 }
656− out, _ = sjson.Set(out, "usageMetadata", usageObj)
657618 }
658619
659620 return out
internal/translator/openai/openai/responses/openai_openai-responses_response.go+35−66
@@ -484,16 +484,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 483 unmodified lines
484484 }
485485 }
486486 // Build response.output using aggregated buffers
487− var outputs []interface{}
487+ outputsWrapper := `{"arr":[]}`
488488 if st.ReasoningBuf.Len() > 0 {
489− outputs = append(outputs, map[string]interface{}{
490− "id": st.ReasoningID,
491− "type": "reasoning",
492− "summary": []interface{}{map[string]interface{}{
493− "type": "summary_text",
494− "text": st.ReasoningBuf.String(),
495− }},
496− })
489+ item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
490+ item, _ = sjson.Set(item, "id", st.ReasoningID)
491+ item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
492+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
497493 }
498494 // Append message items in ascending index order
499495 if len(st.MsgItemAdded) > 0 {
@@ -513,18 +509,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 13 unmodified lines
513509 if b := st.MsgTextBuf[i]; b != nil {
514510 txt = b.String()
515511 }
516− outputs = append(outputs, map[string]interface{}{
517− "id": fmt.Sprintf("msg_%s_%d", st.ResponseID, i),
518− "type": "message",
519− "status": "completed",
520− "content": []interface{}{map[string]interface{}{
521− "type": "output_text",
522− "annotations": []interface{}{},
523− "logprobs": []interface{}{},
524− "text": txt,
525− }},
526− "role": "assistant",
527− })
512+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
513+ item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
514+ item, _ = sjson.Set(item, "content.0.text", txt)
515+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
528516 }
529517 }
530518 if len(st.FuncArgsBuf) > 0 {
@@ -547,18 +535,16 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 16 unmodified lines
547535 }
548536 callID := st.FuncCallIDs[i]
549537 name := st.FuncNames[i]
550− outputs = append(outputs, map[string]interface{}{
551− "id": fmt.Sprintf("fc_%s", callID),
552− "type": "function_call",
553− "status": "completed",
554− "arguments": args,
555− "call_id": callID,
556− "name": name,
557− })
538+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
539+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
540+ item, _ = sjson.Set(item, "arguments", args)
541+ item, _ = sjson.Set(item, "call_id", callID)
542+ item, _ = sjson.Set(item, "name", name)
543+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
558544 }
559545 }
560− if len(outputs) > 0 {
561− completed, _ = sjson.Set(completed, "response.output", outputs)
546+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
547+ completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
562548 }
563549 if st.UsageSeen {
564550 completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
@@ -681,7 +667,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 116 unmodified lines
681667 }
682668
683669 // Build output list from choices[...]
684− var outputs []interface{}
670+ outputsWrapper := `{"arr":[]}`
685671 // Detect and capture reasoning content if present
686672 rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
687673 includeReasoning := rcText != ""
@@ -693,21 +679,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 5 unmodified lines
693679 if strings.HasPrefix(rid, "resp_") {
694680 rid = strings.TrimPrefix(rid, "resp_")
695681 }
696− reasoningItem := map[string]interface{}{
697− "id": fmt.Sprintf("rs_%s", rid),
698− "type": "reasoning",
699− "encrypted_content": "",
700− }
701682 // Prefer summary_text from reasoning_content; encrypted_content is optional
702− var summaries []interface{}
683+ reasoningItem := `{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`
684+ reasoningItem, _ = sjson.Set(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
703685 if rcText != "" {
704− summaries = append(summaries, map[string]interface{}{
705− "type": "summary_text",
706− "text": rcText,
707− })
686+ reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text")
687+ reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText)
708688 }
709− reasoningItem["summary"] = summaries
710− outputs = append(outputs, reasoningItem)
689+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem)
711690 }
712691
713692 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
@@ -716,18 +695,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 2 unmodified lines
716695 if msg.Exists() {
717696 // Text message part
718697 if c := msg.Get("content"); c.Exists() && c.String() != "" {
719− outputs = append(outputs, map[string]interface{}{
720− "id": fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())),
721− "type": "message",
722− "status": "completed",
723− "content": []interface{}{map[string]interface{}{
724− "type": "output_text",
725− "annotations": []interface{}{},
726− "logprobs": []interface{}{},
727− "text": c.String(),
728− }},
729− "role": "assistant",
730− })
698+ item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
699+ item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
700+ item, _ = sjson.Set(item, "content.0.text", c.String())
701+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
731702 }
732703
733704 // Function/tool calls
@@ -736,14 +707,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 2 unmodified lines
736707 callID := tc.Get("id").String()
737708 name := tc.Get("function.name").String()
738709 args := tc.Get("function.arguments").String()
739− outputs = append(outputs, map[string]interface{}{
740− "id": fmt.Sprintf("fc_%s", callID),
741− "type": "function_call",
742− "status": "completed",
743− "arguments": args,
744− "call_id": callID,
745− "name": name,
746− })
710+ item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
711+ item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
712+ item, _ = sjson.Set(item, "arguments", args)
713+ item, _ = sjson.Set(item, "call_id", callID)
714+ item, _ = sjson.Set(item, "name", name)
715+ outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
747716 return true
748717 })
749718 }
@@ -751,8 +720,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 1 unmodified line
751720 return true
752721 })
753722 }
754− if len(outputs) > 0 {
755− resp, _ = sjson.Set(resp, "output", outputs)
723+ if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
724+ resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw)
756725 }
757726
758727 // usage mapping