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
452 toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))452 toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
453 toolBlock, _ = sjson.Set(toolBlock, "name", name)453 toolBlock, _ = sjson.Set(toolBlock, "name", name)
454 454
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() {
456 toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)456 toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw)
457 }457 }
458 458
internal/translator/antigravity/gemini/antigravity_gemini_request.go+43−65
@@ -7,7 +7,6 @@ package gemini · 6 unmodified lines
7 7
8import (8import (
9 "bytes"9 "bytes"
10 "encoding/json"
11 "fmt"10 "fmt"
12 11
13 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"12 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
@@ -135,41 +134,31 @@ func ConvertGeminiRequestToAntigravity(_ string, inputRawJSON []byte, _ bool) [] · 121 unmodified lines
135 134
136// FunctionCallGroup represents a group of function calls and their responses135// FunctionCallGroup represents a group of function calls and their responses
137type FunctionCallGroup struct {136type FunctionCallGroup struct {
138 ModelContent map[string]interface{}
139 FunctionCalls []gjson.Result
140 ResponsesNeeded int137 ResponsesNeeded int
141}138}
142 139
143// parseFunctionResponse attempts to unmarshal a function response part.140// parseFunctionResponseRaw attempts to normalize a function response part into a JSON object string.
144// Falls back to gjson extraction if standard json.Unmarshal fails.141// Falls back to a minimal "functionResponse" object when parsing fails.
145func parseFunctionResponse(response gjson.Result) map[string]interface{} {142func parseFunctionResponseRaw(response gjson.Result) string {
146 var responseMap map[string]interface{}143 if response.IsObject() && gjson.Valid(response.Raw) {
147 err := json.Unmarshal([]byte(response.Raw), &responseMap)144 return response.Raw
148 if err == nil {
149 return responseMap
150 }145 }
151 146
152 log.Debugf("unmarshal function response failed, using fallback: %v", err)147 log.Debugf("parse function response failed, using fallback")
153 funcResp := response.Get("functionResponse")148 funcResp := response.Get("functionResponse")
154 if funcResp.Exists() {149 if funcResp.Exists() {
155 fr := map[string]interface{}{150 fr := `{"functionResponse":{"name":"","response":{"result":""}}}`
156 "name": funcResp.Get("name").String(),151 fr, _ = sjson.Set(fr, "functionResponse.name", funcResp.Get("name").String())
157 "response": map[string]interface{}{152 fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String())
158 "result": funcResp.Get("response").String(),
159 },
160 }
161 if id := funcResp.Get("id").String(); id != "" {153 if id := funcResp.Get("id").String(); id != "" {
162 fr["id"] = id154 fr, _ = sjson.Set(fr, "functionResponse.id", id)
163 }155 }
164 return map[string]interface{}{"functionResponse": fr}156 return fr
165 }157 }
166 158
167 return map[string]interface{}{159 fr := `{"functionResponse":{"name":"unknown","response":{"result":""}}}`
168 "functionResponse": map[string]interface{}{160 fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String())
169 "name": "unknown",161 return fr
170 "response": map[string]interface{}{"result": response.String()},
171 },
172 }
173}162}
174 163
175// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.164// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
@@ -196,7 +185,7 @@ func fixCLIToolResponse(input string) (string, error) { · 20 unmodified lines
196 }185 }
197 186
198 // Initialize data structures for processing and grouping187 // Initialize data structures for processing and grouping
199 var newContents []interface{} // Final processed contents array188 contentsWrapper := `{"contents":[]}`
200 var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses189 var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
201 var collectedResponses []gjson.Result // Standalone responses to be matched190 var collectedResponses []gjson.Result // Standalone responses to be matched
202 191
@@ -228,17 +217,16 @@ func fixCLIToolResponse(input string) (string, error) { · 25 unmodified lines
228 collectedResponses = collectedResponses[group.ResponsesNeeded:]217 collectedResponses = collectedResponses[group.ResponsesNeeded:]
229 218
230 // Create merged function response content219 // Create merged function response content
231 var responseParts []interface{}220 functionResponseContent := `{"parts":[],"role":"function"}`
232 for _, response := range groupResponses {221 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 }
234 }226 }
235 227
236 if len(responseParts) > 0 {228 if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
237 functionResponseContent := map[string]interface{}{229 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
238 "parts": responseParts,
239 "role": "function",
240 }
241 newContents = append(newContents, functionResponseContent)
242 }230 }
243 231
244 // Remove this group as it's been satisfied232 // Remove this group as it's been satisfied
@@ -252,50 +240,42 @@ func fixCLIToolResponse(input string) (string, error) { · 7 unmodified lines
252 240
253 // If this is a model with function calls, create a new group241 // If this is a model with function calls, create a new group
254 if role == "model" {242 if role == "model" {
255 var functionCallsInThisModel []gjson.Result243 functionCallsCount := 0
256 parts.ForEach(func(_, part gjson.Result) bool {244 parts.ForEach(func(_, part gjson.Result) bool {
257 if part.Get("functionCall").Exists() {245 if part.Get("functionCall").Exists() {
258 functionCallsInThisModel = append(functionCallsInThisModel, part)246 functionCallsCount++
259 }247 }
260 return true248 return true
261 })249 })
262 250
263 if len(functionCallsInThisModel) > 0 {251 if functionCallsCount > 0 {
264 // Add the model content252 // Add the model content
265 var contentMap map[string]interface{}253 if !value.IsObject() {
266 errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)254 log.Warnf("failed to parse model content")
267 if errUnmarshal != nil {
268 log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
269 return true255 return true
270 }256 }
271 newContents = append(newContents, contentMap)257 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
272 258
273 // Create a new group for tracking responses259 // Create a new group for tracking responses
274 group := &FunctionCallGroup{260 group := &FunctionCallGroup{
275 ModelContent: contentMap,261 ResponsesNeeded: functionCallsCount,
276 FunctionCalls: functionCallsInThisModel,
277 ResponsesNeeded: len(functionCallsInThisModel),
278 }262 }
279 pendingGroups = append(pendingGroups, group)263 pendingGroups = append(pendingGroups, group)
280 } else {264 } else {
281 // Regular model content without function calls265 // Regular model content without function calls
282 var contentMap map[string]interface{}266 if !value.IsObject() {
283 errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)267 log.Warnf("failed to parse content")
284 if errUnmarshal != nil {
285 log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
286 return true268 return true
287 }269 }
288 newContents = append(newContents, contentMap)270 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
289 }271 }
290 } else {272 } else {
291 // Non-model content (user, etc.)273 // Non-model content (user, etc.)
292 var contentMap map[string]interface{}274 if !value.IsObject() {
293 errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)275 log.Warnf("failed to parse content")
294 if errUnmarshal != nil {
295 log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
296 return true276 return true
297 }277 }
298 newContents = append(newContents, contentMap)278 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
299 }279 }
300 280
301 return true281 return true
@@ -307,25 +287,23 @@ func fixCLIToolResponse(input string) (string, error) { · 5 unmodified lines
307 groupResponses := collectedResponses[:group.ResponsesNeeded]287 groupResponses := collectedResponses[:group.ResponsesNeeded]
308 collectedResponses = collectedResponses[group.ResponsesNeeded:]288 collectedResponses = collectedResponses[group.ResponsesNeeded:]
309 289
310 var responseParts []interface{}290 functionResponseContent := `{"parts":[],"role":"function"}`
311 for _, response := range groupResponses {291 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 }
313 }296 }
314 297
315 if len(responseParts) > 0 {298 if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
316 functionResponseContent := map[string]interface{}{299 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
317 "parts": responseParts,
318 "role": "function",
319 }
320 newContents = append(newContents, functionResponseContent)
321 }300 }
322 }301 }
323 }302 }
324 303
325 // Update the original JSON with the new contents304 // Update the original JSON with the new contents
326 result := input305 result := input
327 newContentsJSON, _ := json.Marshal(newContents)306 result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
328 result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
329 307
330 return result, nil308 return result, nil
331}309}
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
319 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)319 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
320 continue320 continue
321 }321 }
322 fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})322 fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
323 if errSet != nil {323 if errSet != nil {
324 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)324 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
325 continue325 continue
@@ -334,7 +334,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 8 unmodified lines
334 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)334 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
335 continue335 continue
336 }336 }
337 fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})337 fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
338 if errSet != nil {338 if errSet != nil {
339 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)339 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
340 continue340 continue
internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go+3−11
@@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines
8import (8import (
9 "bytes"9 "bytes"
10 "context"10 "context"
11 "encoding/json"
12 "fmt"11 "fmt"
13 "strings"12 "strings"
14 "sync/atomic"13 "sync/atomic"
@@ -171,21 +170,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq · 156 unmodified lines
171 mimeType = "image/png"170 mimeType = "image/png"
172 }171 }
173 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)172 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
174 imagePayload, err := json.Marshal(map[string]any{173 imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
175 "type": "image_url",174 imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
176 "image_url": map[string]string{
177 "url": imageURL,
178 },
179 })
180 if err != nil {
181 continue
182 }
183 imagesResult := gjson.Get(template, "choices.0.delta.images")175 imagesResult := gjson.Get(template, "choices.0.delta.images")
184 if !imagesResult.Exists() || !imagesResult.IsArray() {176 if !imagesResult.Exists() || !imagesResult.IsArray() {
185 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)177 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
186 }178 }
187 template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")179 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)
189 }181 }
190 }182 }
191 }183 }
internal/translator/claude/gemini/claude_gemini_request.go+4−4
@@ -194,7 +194,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream · 193 unmodified lines
194 if name := fc.Get("name"); name.Exists() {194 if name := fc.Get("name"); name.Exists() {
195 toolUse, _ = sjson.Set(toolUse, "name", name.String())195 toolUse, _ = sjson.Set(toolUse, "name", name.String())
196 }196 }
197 if args := fc.Get("args"); args.Exists() {197 if args := fc.Get("args"); args.Exists() && args.IsObject() {
198 toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)198 toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw)
199 }199 }
200 msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)200 msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
@@ -314,11 +314,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream · 113 unmodified lines
314 if mode := funcCalling.Get("mode"); mode.Exists() {314 if mode := funcCalling.Get("mode"); mode.Exists() {
315 switch mode.String() {315 switch mode.String() {
316 case "AUTO":316 case "AUTO":
317 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})317 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
318 case "NONE":318 case "NONE":
319 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "none"})319 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"none"}`)
320 case "ANY":320 case "ANY":
321 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})321 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
322 }322 }
323 }323 }
324 }324 }
internal/translator/claude/gemini/claude_gemini_response.go+28−92
@@ -263,51 +263,6 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original · 262 unmodified lines
263 }263 }
264}264}
265 265
266// convertArrayToJSON converts []interface{} to JSON array string
267func 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
288func 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
311// ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response.266// ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response.
312// This function processes the complete Claude Code response and transforms it into a single Gemini-compatible267// This function processes the complete Claude Code response and transforms it into a single Gemini-compatible
313// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all268// 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
356 }311 }
357 312
358 // Process each streaming event and collect parts313 // Process each streaming event and collect parts
359 var allParts []interface{}314 var allParts []string
360 var finalUsage map[string]interface{}315 var finalUsageJSON string
361 var responseID string316 var responseID string
362 var createdAt int64317 var createdAt int64
363 318
@@ -407,16 +362,14 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 43 unmodified lines
407 if text := delta.Get("text"); text.Exists() && text.String() != "" {362 if text := delta.Get("text"); text.Exists() && text.String() != "" {
408 partJSON := `{"text":""}`363 partJSON := `{"text":""}`
409 partJSON, _ = sjson.Set(partJSON, "text", text.String())364 partJSON, _ = sjson.Set(partJSON, "text", text.String())
410 part := gjson.Parse(partJSON).Value().(map[string]interface{})365 allParts = append(allParts, partJSON)
411 allParts = append(allParts, part)
412 }366 }
413 case "thinking_delta":367 case "thinking_delta":
414 // Process reasoning/thinking content368 // Process reasoning/thinking content
415 if text := delta.Get("thinking"); text.Exists() && text.String() != "" {369 if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
416 partJSON := `{"thought":true,"text":""}`370 partJSON := `{"thought":true,"text":""}`
417 partJSON, _ = sjson.Set(partJSON, "text", text.String())371 partJSON, _ = sjson.Set(partJSON, "text", text.String())
418 part := gjson.Parse(partJSON).Value().(map[string]interface{})372 allParts = append(allParts, partJSON)
419 allParts = append(allParts, part)
420 }373 }
421 case "input_json_delta":374 case "input_json_delta":
422 // accumulate args partial_json for this index375 // accumulate args partial_json for this index
@@ -456,9 +409,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 33 unmodified lines
456 if argsTrim != "" {409 if argsTrim != "" {
457 functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)410 functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
458 }411 }
459 // Parse back to interface{} for allParts412 allParts = append(allParts, functionCallJSON)
460 functionCall := gjson.Parse(functionCallJSON).Value().(map[string]interface{})
461 allParts = append(allParts, functionCall)
462 // cleanup used state for this index413 // cleanup used state for this index
463 if newParam.ToolUseArgs != nil {414 if newParam.ToolUseArgs != nil {
464 delete(newParam.ToolUseArgs, idx)415 delete(newParam.ToolUseArgs, idx)
@@ -501,8 +452,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 36 unmodified lines
501 // Set traffic type (required by Gemini API)452 // Set traffic type (required by Gemini API)
502 usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")453 usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
503 454
504 // Convert to map[string]interface{} using gjson455 finalUsageJSON = usageJSON
505 finalUsage = gjson.Parse(usageJSON).Value().(map[string]interface{})
506 }456 }
507 }457 }
508 }458 }
@@ -520,12 +470,16 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 11 unmodified lines
520 470
521 // Set the consolidated parts array471 // Set the consolidated parts array
522 if len(consolidatedParts) > 0 {472 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)
524 }478 }
525 479
526 // Set usage metadata480 // Set usage metadata
527 if finalUsage != nil {481 if finalUsageJSON != "" {
528 template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage))482 template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON)
529 }483 }
530 484
531 return template485 return template
@@ -539,12 +493,12 @@ func GeminiTokenCount(ctx context.Context, count int64) string { · 7 unmodified lines
539// This function processes the parts array to combine adjacent text elements and thinking elements493// This function processes the parts array to combine adjacent text elements and thinking elements
540// into single consolidated parts, which results in a more readable and efficient response structure.494// into single consolidated parts, which results in a more readable and efficient response structure.
541// Tool calls and other non-text parts are preserved as separate elements.495// Tool calls and other non-text parts are preserved as separate elements.
542func consolidateParts(parts []interface{}) []interface{} {496func consolidateParts(parts []string) []string {
543 if len(parts) == 0 {497 if len(parts) == 0 {
544 return parts498 return parts
545 }499 }
546 500
547 var consolidated []interface{}501 var consolidated []string
548 var currentTextPart strings.Builder502 var currentTextPart strings.Builder
549 var currentThoughtPart strings.Builder503 var currentThoughtPart strings.Builder
550 var hasText, hasThought bool504 var hasText, hasThought bool
@@ -554,8 +508,7 @@ func consolidateParts(parts []interface{}) []interface{} { · 3 unmodified lines
554 if hasText && currentTextPart.Len() > 0 {508 if hasText && currentTextPart.Len() > 0 {
555 textPartJSON := `{"text":""}`509 textPartJSON := `{"text":""}`
556 textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())510 textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
557 textPart := gjson.Parse(textPartJSON).Value().(map[string]interface{})511 consolidated = append(consolidated, textPartJSON)
558 consolidated = append(consolidated, textPart)
559 currentTextPart.Reset()512 currentTextPart.Reset()
560 hasText = false513 hasText = false
561 }514 }
@@ -566,42 +519,42 @@ func consolidateParts(parts []interface{}) []interface{} { · 4 unmodified lines
566 if hasThought && currentThoughtPart.Len() > 0 {519 if hasThought && currentThoughtPart.Len() > 0 {
567 thoughtPartJSON := `{"thought":true,"text":""}`520 thoughtPartJSON := `{"thought":true,"text":""}`
568 thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())521 thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
569 thoughtPart := gjson.Parse(thoughtPartJSON).Value().(map[string]interface{})522 consolidated = append(consolidated, thoughtPartJSON)
570 consolidated = append(consolidated, thoughtPart)
571 currentThoughtPart.Reset()523 currentThoughtPart.Reset()
572 hasThought = false524 hasThought = false
573 }525 }
574 }526 }
575 527
576 for _, part := range parts {528 for _, partJSON := range parts {
577 partMap, ok := part.(map[string]interface{})529 part := gjson.Parse(partJSON)
578 if !ok {530 if !part.Exists() || !part.IsObject() {
579 // Flush any pending parts and add this non-text part531 // Flush any pending parts and add this non-text part
580 flushText()532 flushText()
581 flushThought()533 flushThought()
582 consolidated = append(consolidated, part)534 consolidated = append(consolidated, partJSON)
583 continue535 continue
584 }536 }
585 537
586 if thought, isThought := partMap["thought"]; isThought && thought == true {538 thought := part.Get("thought")
539 if thought.Exists() && thought.Type == gjson.True {
587 // This is a thinking part - flush any pending text first540 // This is a thinking part - flush any pending text first
588 flushText() // Flush any pending text first541 flushText() // Flush any pending text first
589 542
590 if text, hasTextContent := partMap["text"].(string); hasTextContent {543 if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
591 currentThoughtPart.WriteString(text)544 currentThoughtPart.WriteString(text.String())
592 hasThought = true545 hasThought = true
593 }546 }
594 } else if text, hasTextContent := partMap["text"].(string); hasTextContent {547 } else if text := part.Get("text"); text.Exists() && text.Type == gjson.String {
595 // This is a regular text part - flush any pending thought first548 // This is a regular text part - flush any pending thought first
596 flushThought() // Flush any pending thought first549 flushThought() // Flush any pending thought first
597 550
598 currentTextPart.WriteString(text)551 currentTextPart.WriteString(text.String())
599 hasText = true552 hasText = true
600 } else {553 } else {
601 // This is some other type of part (like function call) - flush both text and thought554 // This is some other type of part (like function call) - flush both text and thought
602 flushText()555 flushText()
603 flushThought()556 flushThought()
604 consolidated = append(consolidated, part)557 consolidated = append(consolidated, partJSON)
605 }558 }
606 }559 }
607 560
@@ -611,20 +564,3 @@ func consolidateParts(parts []interface{}) []interface{} { · 3 unmodified lines
611 564
612 return consolidated565 return consolidated
613}566}
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.
618func 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
10 "crypto/rand"10 "crypto/rand"
11 "crypto/sha256"11 "crypto/sha256"
12 "encoding/hex"12 "encoding/hex"
13 "encoding/json"
14 "fmt"13 "fmt"
15 "math/big"14 "math/big"
16 "strings"15 "strings"
@@ -137,9 +136,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 120 unmodified lines
137 out, _ = sjson.Set(out, "stream", stream)136 out, _ = sjson.Set(out, "stream", stream)
138 137
139 // Process messages and transform them to Claude Code format138 // 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
143 if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {139 if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
144 messages.ForEach(func(_, message gjson.Result) bool {140 messages.ForEach(func(_, message gjson.Result) bool {
145 role := message.Get("role").String()141 role := message.Get("role").String()
@@ -152,33 +148,23 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines
152 role = "user"148 role = "user"
153 }149 }
154 150
155 msg := map[string]interface{}{151 msg := `{"role":"","content":[]}`
156 "role": role,152 msg, _ = sjson.Set(msg, "role", role)
157 "content": []interface{}{},
158 }
159 153
160 // Handle content based on its type (string or array)154 // Handle content based on its type (string or array)
161 if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {155 if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
162 // Simple text content conversion156 part := `{"type":"text","text":""}`
163 msg["content"] = []interface{}{157 part, _ = sjson.Set(part, "text", contentResult.String())
164 map[string]interface{}{158 msg, _ = sjson.SetRaw(msg, "content.-1", part)
165 "type": "text",
166 "text": contentResult.String(),
167 },
168 }
169 } else if contentResult.Exists() && contentResult.IsArray() {159 } else if contentResult.Exists() && contentResult.IsArray() {
170 // Array of content parts processing
171 var contentParts []interface{}
172 contentResult.ForEach(func(_, part gjson.Result) bool {160 contentResult.ForEach(func(_, part gjson.Result) bool {
173 partType := part.Get("type").String()161 partType := part.Get("type").String()
174 162
175 switch partType {163 switch partType {
176 case "text":164 case "text":
177 // Text part conversion165 textPart := `{"type":"text","text":""}`
178 contentParts = append(contentParts, map[string]interface{}{166 textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
179 "type": "text",167 msg, _ = sjson.SetRaw(msg, "content.-1", textPart)
180 "text": part.Get("text").String(),
181 })
182 168
183 case "image_url":169 case "image_url":
184 // Convert OpenAI image format to Claude Code format170 // Convert OpenAI image format to Claude Code format
@@ -191,132 +177,95 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines
191 mediaType := strings.TrimPrefix(mediaTypePart, "data:")177 mediaType := strings.TrimPrefix(mediaTypePart, "data:")
192 data := parts[1]178 data := parts[1]
193 179
194 contentParts = append(contentParts, map[string]interface{}{180 imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
195 "type": "image",181 imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
196 "source": map[string]interface{}{182 imagePart, _ = sjson.Set(imagePart, "source.data", data)
197 "type": "base64",183 msg, _ = sjson.SetRaw(msg, "content.-1", imagePart)
198 "media_type": mediaType,
199 "data": data,
200 },
201 })
202 }184 }
203 }185 }
204 }186 }
205 return true187 return true
206 })188 })
207 if len(contentParts) > 0 {
208 msg["content"] = contentParts
209 }
210 } else {
211 // Initialize empty content array for tool calls
212 msg["content"] = []interface{}{}
213 }189 }
214 190
215 // Handle tool calls (for assistant messages)191 // Handle tool calls (for assistant messages)
216 if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() && role == "assistant" {192 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
224 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {193 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
225 if toolCall.Get("type").String() == "function" {194 if toolCall.Get("type").String() == "function" {
226 toolCallID := toolCall.Get("id").String()195 toolCallID := toolCall.Get("id").String()
227 if toolCallID == "" {196 if toolCallID == "" {
228 toolCallID = genToolCallID()197 toolCallID = genToolCallID()
229 }198 }
230 toolCallIDs = append(toolCallIDs, toolCallID)
231 199
232 function := toolCall.Get("function")200 function := toolCall.Get("function")
233 toolUse := map[string]interface{}{201 toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
234 "type": "tool_use",202 toolUse, _ = sjson.Set(toolUse, "id", toolCallID)
235 "id": toolCallID,203 toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String())
236 "name": function.Get("name").String(),
237 }
238 204
239 // Parse arguments for the tool call205 // Parse arguments for the tool call
240 if args := function.Get("arguments"); args.Exists() {206 if args := function.Get("arguments"); args.Exists() {
241 argsStr := args.String()207 argsStr := args.String()
242 if argsStr != "" {208 if argsStr != "" && gjson.Valid(argsStr) {
243 var argsMap map[string]interface{}209 argsJSON := gjson.Parse(argsStr)
244 if err := json.Unmarshal([]byte(argsStr), &argsMap); err == nil {210 if argsJSON.IsObject() {
245 toolUse["input"] = argsMap211 toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
246 } else {212 } else {
247 toolUse["input"] = map[string]interface{}{}213 toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
248 }214 }
249 } else {215 } else {
250 toolUse["input"] = map[string]interface{}{}216 toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
251 }217 }
252 } else {218 } else {
253 toolUse["input"] = map[string]interface{}{}219 toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
254 }220 }
255 221
256 contentParts = append(contentParts, toolUse)222 msg, _ = sjson.SetRaw(msg, "content.-1", toolUse)
257 }223 }
258 return true224 return true
259 })225 })
260 msg["content"] = contentParts
261 }226 }
262 227
263 anthropicMessages = append(anthropicMessages, msg)228 out, _ = sjson.SetRaw(out, "messages.-1", msg)
264 229
265 case "tool":230 case "tool":
266 // Handle tool result messages conversion231 // Handle tool result messages conversion
267 toolCallID := message.Get("tool_call_id").String()232 toolCallID := message.Get("tool_call_id").String()
268 content := message.Get("content").String()233 content := message.Get("content").String()
269 234
270 // Create tool result message in Claude Code format235 msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
271 msg := map[string]interface{}{236 msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
272 "role": "user",237 msg, _ = sjson.Set(msg, "content.0.content", content)
273 "content": []interface{}{238 out, _ = sjson.SetRaw(out, "messages.-1", msg)
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)
283 }239 }
284 return true240 return true
285 })241 })
286 }242 }
287 243
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
294 // Tools mapping: OpenAI tools -> Claude Code tools244 // Tools mapping: OpenAI tools -> Claude Code tools
295 if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {245 if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 {
296 var anthropicTools []interface{}246 hasAnthropicTools := false
297 tools.ForEach(func(_, tool gjson.Result) bool {247 tools.ForEach(func(_, tool gjson.Result) bool {
298 if tool.Get("type").String() == "function" {248 if tool.Get("type").String() == "function" {
299 function := tool.Get("function")249 function := tool.Get("function")
300 anthropicTool := map[string]interface{}{250 anthropicTool := `{"name":"","description":""}`
301 "name": function.Get("name").String(),251 anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String())
302 "description": function.Get("description").String(),252 anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String())
303 }
304 253
305 // Convert parameters schema for the tool254 // Convert parameters schema for the tool
306 if parameters := function.Get("parameters"); parameters.Exists() {255 if parameters := function.Get("parameters"); parameters.Exists() {
307 anthropicTool["input_schema"] = parameters.Value()256 anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
308 } else if parameters = function.Get("parametersJsonSchema"); parameters.Exists() {257 } else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() {
309 anthropicTool["input_schema"] = parameters.Value()258 anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw)
310 }259 }
311 260
312 anthropicTools = append(anthropicTools, anthropicTool)261 out, _ = sjson.SetRaw(out, "tools.-1", anthropicTool)
262 hasAnthropicTools = true
313 }263 }
314 return true264 return true
315 })265 })
316 266
317 if len(anthropicTools) > 0 {267 if !hasAnthropicTools {
318 toolsJSON, _ := json.Marshal(anthropicTools)268 out, _ = sjson.Delete(out, "tools")
319 out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
320 }269 }
321 }270 }
322 271
@@ -329,18 +278,17 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines
329 case "none":278 case "none":
330 // Don't set tool_choice, Claude Code will not use tools279 // Don't set tool_choice, Claude Code will not use tools
331 case "auto":280 case "auto":
332 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})281 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
333 case "required":282 case "required":
334 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})283 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
335 }284 }
336 case gjson.JSON:285 case gjson.JSON:
337 // Specific tool choice mapping286 // Specific tool choice mapping
338 if toolChoice.Get("type").String() == "function" {287 if toolChoice.Get("type").String() == "function" {
339 functionName := toolChoice.Get("function.name").String()288 functionName := toolChoice.Get("function.name").String()
340 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{289 toolChoiceJSON := `{"type":"tool","name":""}`
341 "type": "tool",290 toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName)
342 "name": functionName,291 out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
343 })
344 }292 }
345 default:293 default:
346 }294 }
internal/translator/claude/openai/chat-completions/claude_openai_response.go+46−62
@@ -8,7 +8,7 @@ package chat_completions · 7 unmodified lines
8import (8import (
9 "bytes"9 "bytes"
10 "context"10 "context"
11 "encoding/json"11 "fmt"
12 "strings"12 "strings"
13 "time"13 "time"
14 14
@@ -178,18 +178,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 163 unmodified lines
178 if arguments == "" {178 if arguments == "" {
179 arguments = "{}"179 arguments = "{}"
180 }180 }
181 181 template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index)
182 toolCall := map[string]interface{}{182 template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID)
183 "index": index,183 template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function")
184 "id": accumulator.ID,184 template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name)
185 "type": "function",185 template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.arguments", arguments)
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})
193 186
194 // Clean up the accumulator for this index187 // Clean up the accumulator for this index
195 delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)188 delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
@@ -210,12 +203,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 14 unmodified lines
210 203
211 // Handle usage information for token counts204 // Handle usage information for token counts
212 if usage := root.Get("usage"); usage.Exists() {205 if usage := root.Get("usage"); usage.Exists() {
213 usageObj := map[string]interface{}{206 inputTokens := usage.Get("input_tokens").Int()
214 "prompt_tokens": usage.Get("input_tokens").Int(),207 outputTokens := usage.Get("output_tokens").Int()
215 "completion_tokens": usage.Get("output_tokens").Int(),208 template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens)
216 "total_tokens": usage.Get("input_tokens").Int() + usage.Get("output_tokens").Int(),209 template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens)
217 }210 template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens)
218 template, _ = sjson.Set(template, "usage", usageObj)
219 }211 }
220 return []string{template}212 return []string{template}
221 213
@@ -230,14 +222,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 8 unmodified lines
230 case "error":222 case "error":
231 // Error event - format and return error response223 // Error event - format and return error response
232 if errorData := root.Get("error"); errorData.Exists() {224 if errorData := root.Get("error"); errorData.Exists() {
233 errorResponse := map[string]interface{}{225 errorJSON := `{"error":{"message":"","type":""}}`
234 "error": map[string]interface{}{226 errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String())
235 "message": errorData.Get("message").String(),227 errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String())
236 "type": errorData.Get("type").String(),228 return []string{errorJSON}
237 },
238 }
239 errorJSON, _ := json.Marshal(errorResponse)
240 return []string{string(errorJSON)}
241 }229 }
242 return []string{}230 return []string{}
243 231
@@ -298,10 +286,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 54 unmodified lines
298 var stopReason string286 var stopReason string
299 var contentParts []string287 var contentParts []string
300 var reasoningParts []string288 var reasoningParts []string
301 // Use map to track tool calls by index for proper merging289 toolCallsAccumulator := make(map[int]*ToolCallAccumulator)
302 toolCallsMap := make(map[int]map[string]interface{})
303 // Track tool call arguments accumulation
304 toolCallArgsMap := make(map[int]strings.Builder)
305 290
306 for _, chunk := range chunks {291 for _, chunk := range chunks {
307 root := gjson.ParseBytes(chunk)292 root := gjson.ParseBytes(chunk)
@@ -327,18 +312,12 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 19 unmodified lines
327 // Start of thinking/reasoning content - skip for now as it's handled in delta312 // Start of thinking/reasoning content - skip for now as it's handled in delta
328 continue313 continue
329 } else if blockType == "tool_use" {314 } else if blockType == "tool_use" {
330 // Initialize tool call tracking for this index315 // Initialize tool call accumulator for this index
331 index := int(root.Get("index").Int())316 index := int(root.Get("index").Int())
332 toolCallsMap[index] = map[string]interface{}{317 toolCallsAccumulator[index] = &ToolCallAccumulator{
333 "id": contentBlock.Get("id").String(),318 ID: contentBlock.Get("id").String(),
334 "type": "function",319 Name: contentBlock.Get("name").String(),
335 "function": map[string]interface{}{
336 "name": contentBlock.Get("name").String(),
337 "arguments": "",
338 },
339 }320 }
340 // Initialize arguments builder for this tool call
341 toolCallArgsMap[index] = strings.Builder{}
342 }321 }
343 }322 }
344 323
@@ -361,9 +340,8 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 16 unmodified lines
361 // Accumulate tool call arguments340 // Accumulate tool call arguments
362 if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {341 if partialJSON := delta.Get("partial_json"); partialJSON.Exists() {
363 index := int(root.Get("index").Int())342 index := int(root.Get("index").Int())
364 if builder, exists := toolCallArgsMap[index]; exists {343 if accumulator, exists := toolCallsAccumulator[index]; exists {
365 builder.WriteString(partialJSON.String())344 accumulator.Arguments.WriteString(partialJSON.String())
366 toolCallArgsMap[index] = builder
367 }345 }
368 }346 }
369 }347 }
@@ -372,14 +350,9 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 2 unmodified lines
372 case "content_block_stop":350 case "content_block_stop":
373 // Finalize tool call arguments for this index when content block ends351 // Finalize tool call arguments for this index when content block ends
374 index := int(root.Get("index").Int())352 index := int(root.Get("index").Int())
375 if toolCall, exists := toolCallsMap[index]; exists {353 if accumulator, exists := toolCallsAccumulator[index]; exists {
376 if builder, argsExists := toolCallArgsMap[index]; argsExists {354 if accumulator.Arguments.Len() == 0 {
377 // Set the accumulated arguments for the tool call355 accumulator.Arguments.WriteString("{}")
378 arguments := builder.String()
379 if arguments == "" {
380 arguments = "{}"
381 }
382 toolCall["function"].(map[string]interface{})["arguments"] = arguments
383 }356 }
384 }357 }
385 358
@@ -417,24 +390,35 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 31 unmodified lines
417 }390 }
418 391
419 // Set tool calls if any were accumulated during processing392 // Set tool calls if any were accumulated during processing
420 if len(toolCallsMap) > 0 {393 if len(toolCallsAccumulator) > 0 {
421 // Convert tool calls map to array, preserving order by index394 toolCallsCount := 0
422 var toolCallsArray []interface{}
423 // Find the maximum index to determine the range
424 maxIndex := -1395 maxIndex := -1
425 for index := range toolCallsMap {396 for index := range toolCallsAccumulator {
426 if index > maxIndex {397 if index > maxIndex {
427 maxIndex = index398 maxIndex = index
428 }399 }
429 }400 }
430 // Iterate through all possible indices up to maxIndex401
431 for i := 0; i <= maxIndex; i++ {402 for i := 0; i <= maxIndex; i++ {
432 if toolCall, exists := toolCallsMap[i]; exists {403 accumulator, exists := toolCallsAccumulator[i]
433 toolCallsArray = append(toolCallsArray, toolCall)404 if !exists {
405 continue
434 }406 }
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++
435 }420 }
436 if len(toolCallsArray) > 0 {421 if toolCallsCount > 0 {
437 out, _ = sjson.Set(out, "choices.0.message.tool_calls", toolCallsArray)
438 out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")422 out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
439 } else {423 } else {
440 out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))424 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
254 toolUse, _ = sjson.Set(toolUse, "id", callID)254 toolUse, _ = sjson.Set(toolUse, "id", callID)
255 toolUse, _ = sjson.Set(toolUse, "name", name)255 toolUse, _ = sjson.Set(toolUse, "name", name)
256 if argsStr != "" && gjson.Valid(argsStr) {256 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 }
258 }261 }
259 262
260 asst := `{"role":"assistant","content":[]}`263 asst := `{"role":"assistant","content":[]}`
@@ -309,16 +312,18 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte · 48 unmodified lines
309 case gjson.String:312 case gjson.String:
310 switch toolChoice.String() {313 switch toolChoice.String() {
311 case "auto":314 case "auto":
312 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "auto"})315 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
313 case "none":316 case "none":
314 // Leave unset; implies no tools317 // Leave unset; implies no tools
315 case "required":318 case "required":
316 out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{"type": "any"})319 out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
317 }320 }
318 case gjson.JSON:321 case gjson.JSON:
319 if toolChoice.Get("type").String() == "function" {322 if toolChoice.Get("type").String() == "function" {
320 fn := toolChoice.Get("function.name").String()323 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)
322 }327 }
323 default:328 default:
324 329
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
344 }344 }
345 345
346 // Build response.output from aggregated state346 // Build response.output from aggregated state
347 var outputs []interface{}347 outputsWrapper := `{"arr":[]}`
348 // reasoning item (if any)348 // reasoning item (if any)
349 if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {349 if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
350 r := map[string]interface{}{350 item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
351 "id": st.ReasoningItemID,351 item, _ = sjson.Set(item, "id", st.ReasoningItemID)
352 "type": "reasoning",352 item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
353 "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},353 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
354 }
355 outputs = append(outputs, r)
356 }354 }
357 // assistant message item (if any text)355 // assistant message item (if any text)
358 if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {356 if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {
359 m := map[string]interface{}{357 item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
360 "id": st.CurrentMsgID,358 item, _ = sjson.Set(item, "id", st.CurrentMsgID)
361 "type": "message",359 item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
362 "status": "completed",360 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
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)
372 }361 }
373 // function_call items (in ascending index order for determinism)362 // function_call items (in ascending index order for determinism)
374 if len(st.FuncArgsBuf) > 0 {363 if len(st.FuncArgsBuf) > 0 {
@@ -395,19 +384,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin · 20 unmodified lines
395 if callID == "" && st.CurrentFCID != "" {384 if callID == "" && st.CurrentFCID != "" {
396 callID = st.CurrentFCID385 callID = st.CurrentFCID
397 }386 }
398 item := map[string]interface{}{387 item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
399 "id": fmt.Sprintf("fc_%s", callID),388 item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
400 "type": "function_call",389 item, _ = sjson.Set(item, "arguments", args)
401 "status": "completed",390 item, _ = sjson.Set(item, "call_id", callID)
402 "arguments": args,391 item, _ = sjson.Set(item, "name", name)
403 "call_id": callID,392 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
404 "name": name,
405 }
406 outputs = append(outputs, item)
407 }393 }
408 }394 }
409 if len(outputs) > 0 {395 if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
410 completed, _ = sjson.Set(completed, "response.output", outputs)396 completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
411 }397 }
412 398
413 reasoningTokens := int64(0)399 reasoningTokens := int64(0)
@@ -628,27 +614,18 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 214 unmodified lines
628 }614 }
629 615
630 // Build output array616 // Build output array
631 var outputs []interface{}617 outputsWrapper := `{"arr":[]}`
632 if reasoningBuf.Len() > 0 {618 if reasoningBuf.Len() > 0 {
633 outputs = append(outputs, map[string]interface{}{619 item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
634 "id": reasoningItemID,620 item, _ = sjson.Set(item, "id", reasoningItemID)
635 "type": "reasoning",621 item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String())
636 "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": reasoningBuf.String()}},622 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
637 })
638 }623 }
639 if currentMsgID != "" || textBuf.Len() > 0 {624 if currentMsgID != "" || textBuf.Len() > 0 {
640 outputs = append(outputs, map[string]interface{}{625 item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
641 "id": currentMsgID,626 item, _ = sjson.Set(item, "id", currentMsgID)
642 "type": "message",627 item, _ = sjson.Set(item, "content.0.text", textBuf.String())
643 "status": "completed",628 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
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 })
652 }629 }
653 if len(toolCalls) > 0 {630 if len(toolCalls) > 0 {
654 // Preserve index order631 // Preserve index order
@@ -669,18 +646,16 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 14 unmodified lines
669 if args == "" {646 if args == "" {
670 args = "{}"647 args = "{}"
671 }648 }
672 outputs = append(outputs, map[string]interface{}{649 item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
673 "id": fmt.Sprintf("fc_%s", st.id),650 item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id))
674 "type": "function_call",651 item, _ = sjson.Set(item, "arguments", args)
675 "status": "completed",652 item, _ = sjson.Set(item, "call_id", st.id)
676 "arguments": args,653 item, _ = sjson.Set(item, "name", st.name)
677 "call_id": st.id,654 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
678 "name": st.name,
679 })
680 }655 }
681 }656 }
682 if len(outputs) > 0 {657 if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
683 out, _ = sjson.Set(out, "output", outputs)658 out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw)
684 }659 }
685 660
686 // Usage661 // Usage
internal/translator/codex/claude/codex_claude_response.go+31−57
@@ -9,7 +9,6 @@ package claude · 8 unmodified lines
9import (9import (
10 "bytes"10 "bytes"
11 "context"11 "context"
12 "encoding/json"
13 "fmt"12 "fmt"
14 "strings"13 "strings"
15 14
@@ -191,21 +190,12 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 175 unmodified lines
191 return ""190 return ""
192 }191 }
193 192
194 response := map[string]interface{}{193 out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
195 "id": responseData.Get("id").String(),194 out, _ = sjson.Set(out, "id", responseData.Get("id").String())
196 "type": "message",195 out, _ = sjson.Set(out, "model", responseData.Get("model").String())
197 "role": "assistant",196 out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
198 "model": responseData.Get("model").String(),197 out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
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 }
207 198
208 var contentBlocks []interface{}
209 hasToolCall := false199 hasToolCall := false
210 200
211 if output := responseData.Get("output"); output.Exists() && output.IsArray() {201 if output := responseData.Get("output"); output.Exists() && output.IsArray() {
@@ -244,10 +234,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 32 unmodified lines
244 }234 }
245 }235 }
246 if thinkingBuilder.Len() > 0 {236 if thinkingBuilder.Len() > 0 {
247 contentBlocks = append(contentBlocks, map[string]interface{}{237 block := `{"type":"thinking","thinking":""}`
248 "type": "thinking",238 block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
249 "thinking": thinkingBuilder.String(),239 out, _ = sjson.SetRaw(out, "content.-1", block)
250 })
251 }240 }
252 case "message":241 case "message":
253 if content := item.Get("content"); content.Exists() {242 if content := item.Get("content"); content.Exists() {
@@ -256,10 +245,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 2 unmodified lines
256 if part.Get("type").String() == "output_text" {245 if part.Get("type").String() == "output_text" {
257 text := part.Get("text").String()246 text := part.Get("text").String()
258 if text != "" {247 if text != "" {
259 contentBlocks = append(contentBlocks, map[string]interface{}{248 block := `{"type":"text","text":""}`
260 "type": "text",249 block, _ = sjson.Set(block, "text", text)
261 "text": text,250 out, _ = sjson.SetRaw(out, "content.-1", block)
262 })
263 }251 }
264 }252 }
265 return true253 return true
@@ -267,10 +255,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 1 unmodified line
267 } else {255 } else {
268 text := content.String()256 text := content.String()
269 if text != "" {257 if text != "" {
270 contentBlocks = append(contentBlocks, map[string]interface{}{258 block := `{"type":"text","text":""}`
271 "type": "text",259 block, _ = sjson.Set(block, "text", text)
272 "text": text,260 out, _ = sjson.SetRaw(out, "content.-1", block)
273 })
274 }261 }
275 }262 }
276 }263 }
@@ -281,54 +268,41 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 4 unmodified lines
281 name = original268 name = original
282 }269 }
283 270
284 toolBlock := map[string]interface{}{271 toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
285 "type": "tool_use",272 toolBlock, _ = sjson.Set(toolBlock, "id", item.Get("call_id").String())
286 "id": item.Get("call_id").String(),273 toolBlock, _ = sjson.Set(toolBlock, "name", name)
287 "name": name,274 inputRaw := "{}"
288 "input": map[string]interface{}{},275 if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
289 }276 argsJSON := gjson.Parse(argsStr)
290 277 if argsJSON.IsObject() {
291 if argsStr := item.Get("arguments").String(); argsStr != "" {278 inputRaw = argsJSON.Raw
292 var args interface{}
293 if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
294 toolBlock["input"] = args
295 }279 }
296 }280 }
297 281 toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
298 contentBlocks = append(contentBlocks, toolBlock)282 out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
299 }283 }
300 return true284 return true
301 })285 })
302 }286 }
303 287
304 if len(contentBlocks) > 0 {
305 response["content"] = contentBlocks
306 }
307
308 if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {288 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())
310 } else if hasToolCall {290 } else if hasToolCall {
311 response["stop_reason"] = "tool_use"291 out, _ = sjson.Set(out, "stop_reason", "tool_use")
312 } else {292 } else {
313 response["stop_reason"] = "end_turn"293 out, _ = sjson.Set(out, "stop_reason", "end_turn")
314 }294 }
315 295
316 if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" {296 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)
318 }298 }
319 299
320 if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {300 if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() {
321 response["usage"] = map[string]interface{}{301 out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int())
322 "input_tokens": responseData.Get("usage.input_tokens").Int(),302 out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int())
323 "output_tokens": responseData.Get("usage.output_tokens").Int(),
324 }
325 }303 }
326 304
327 responseJSON, err := json.Marshal(response)305 return out
328 if err != nil {
329 return ""
330 }
331 return string(responseJSON)
332}306}
333 307
334// buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools.308// 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
7import (7import (
8 "bytes"8 "bytes"
9 "context"9 "context"
10 "encoding/json"
11 "fmt"10 "fmt"
12 "time"11 "time"
13 12
@@ -190,19 +189,19 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 176 unmodified lines
190 }189 }
191 190
192 // Process output content to build parts array191 // Process output content to build parts array
193 var parts []interface{}
194 hasToolCall := false192 hasToolCall := false
195 var pendingFunctionCalls []interface{}193 var pendingFunctionCalls []string
196 194
197 flushPendingFunctionCalls := func() {195 flushPendingFunctionCalls := func() {
198 if len(pendingFunctionCalls) > 0 {196 if len(pendingFunctionCalls) == 0 {
199 // Add all pending function calls as individual parts197 return
200 // This maintains the original Gemini API format while ensuring consecutive calls are grouped together198 }
201 for _, fc := range pendingFunctionCalls {199 // Add all pending function calls as individual parts
202 parts = append(parts, fc)200 // This maintains the original Gemini API format while ensuring consecutive calls are grouped together
203 }201 for _, fc := range pendingFunctionCalls {
204 pendingFunctionCalls = nil202 template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", fc)
205 }203 }
204 pendingFunctionCalls = nil
206 }205 }
207 206
208 if output := responseData.Get("output"); output.Exists() && output.IsArray() {207 if output := responseData.Get("output"); output.Exists() && output.IsArray() {
@@ -216,11 +215,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 7 unmodified lines
216 215
217 // Add thinking content216 // Add thinking content
218 if content := value.Get("content"); content.Exists() {217 if content := value.Get("content"); content.Exists() {
219 part := map[string]interface{}{218 part := `{"text":"","thought":true}`
220 "thought": true,219 part, _ = sjson.Set(part, "text", content.String())
221 "text": content.String(),220 template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
222 }
223 parts = append(parts, part)
224 }221 }
225 222
226 case "message":223 case "message":
@@ -232,10 +229,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 5 unmodified lines
232 content.ForEach(func(_, contentItem gjson.Result) bool {229 content.ForEach(func(_, contentItem gjson.Result) bool {
233 if contentItem.Get("type").String() == "output_text" {230 if contentItem.Get("type").String() == "output_text" {
234 if text := contentItem.Get("text"); text.Exists() {231 if text := contentItem.Get("text"); text.Exists() {
235 part := map[string]interface{}{232 part := `{"text":""}`
236 "text": text.String(),233 part, _ = sjson.Set(part, "text", text.String())
237 }234 template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
238 parts = append(parts, part)
239 }235 }
240 }236 }
241 return true237 return true
@@ -245,28 +241,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 3 unmodified lines
245 case "function_call":241 case "function_call":
246 // Collect function call for potential merging with consecutive ones242 // Collect function call for potential merging with consecutive ones
247 hasToolCall = true243 hasToolCall = true
248 functionCall := map[string]interface{}{244 functionCall := `{"functionCall":{"args":{},"name":""}}`
249 "functionCall": map[string]interface{}{245 {
250 "name": func() string {246 n := value.Get("name").String()
251 n := value.Get("name").String()247 rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
252 rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)248 if orig, ok := rev[n]; ok {
253 if orig, ok := rev[n]; ok {249 n = orig
254 return orig250 }
255 }251 functionCall, _ = sjson.Set(functionCall, "functionCall.name", n)
256 return n
257 }(),
258 "args": map[string]interface{}{},
259 },
260 }252 }
261 253
262 // Parse and set arguments254 // Parse and set arguments
263 if argsStr := value.Get("arguments").String(); argsStr != "" {255 if argsStr := value.Get("arguments").String(); argsStr != "" {
264 argsResult := gjson.Parse(argsStr)256 argsResult := gjson.Parse(argsStr)
265 if argsResult.IsObject() {257 if argsResult.IsObject() {
266 var args map[string]interface{}258 functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr)
267 if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
268 functionCall["functionCall"].(map[string]interface{})["args"] = args
269 }
270 }259 }
271 }260 }
272 261
@@ -279,11 +268,6 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 6 unmodified lines
279 flushPendingFunctionCalls()268 flushPendingFunctionCalls()
280 }269 }
281 270
282 // Set the parts array
283 if len(parts) > 0 {
284 template, _ = sjson.SetRaw(template, "candidates.0.content.parts", mustMarshalJSON(parts))
285 }
286
287 // Set finish reason based on whether there were tool calls271 // Set finish reason based on whether there were tool calls
288 if hasToolCall {272 if hasToolCall {
289 template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")273 template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
@@ -323,15 +307,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { · 33 unmodified lines
323 return rev307 return rev
324}308}
325 309
326// mustMarshalJSON marshals a value to JSON, panicking on error.
327func mustMarshalJSON(v interface{}) string {
328 data, err := json.Marshal(v)
329 if err != nil {
330 return ""
331 }
332 return string(data)
333}
334
335func GeminiTokenCount(ctx context.Context, count int64) string {310func GeminiTokenCount(ctx context.Context, count int64) string {
336 return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)311 return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
337}312}
internal/translator/gemini-cli/claude/gemini-cli_claude_request.go+78−83
@@ -7,10 +7,8 @@ package claude · 6 unmodified lines
7 7
8import (8import (
9 "bytes"9 "bytes"
10 "encoding/json"
11 "strings"10 "strings"
12 11
13 client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"12 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"13 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
16 "github.com/tidwall/gjson"14 "github.com/tidwall/gjson"
@@ -41,92 +39,100 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 24 unmodified lines
41 rawJSON := bytes.Clone(inputRawJSON)39 rawJSON := bytes.Clone(inputRawJSON)
42 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)40 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
43 41
42 // Build output Gemini CLI request JSON
43 out := `{"model":"","request":{"contents":[]}}`
44 out, _ = sjson.Set(out, "model", modelName)
45
44 // system instruction46 // system instruction
45 var systemInstruction *client.Content47 if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
46 systemResult := gjson.GetBytes(rawJSON, "system")48 systemInstruction := `{"role":"user","parts":[]}`
47 if systemResult.IsArray() {49 hasSystemParts := false
48 systemResults := systemResult.Array()50 systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
49 systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}51 if systemPromptResult.Get("type").String() == "text" {
50 for i := 0; i < len(systemResults); i++ {52 textResult := systemPromptResult.Get("text")
51 systemPromptResult := systemResults[i]53 if textResult.Type == gjson.String {
52 systemTypePromptResult := systemPromptResult.Get("type")54 part := `{"text":""}`
53 if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {55 part, _ = sjson.Set(part, "text", textResult.String())
54 systemPrompt := systemPromptResult.Get("text").String()56 systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
55 systemPart := client.Part{Text: systemPrompt}57 hasSystemParts = true
56 systemInstruction.Parts = append(systemInstruction.Parts, systemPart)58 }
57 }59 }
58 }60 return true
59 if len(systemInstruction.Parts) == 0 {61 })
60 systemInstruction = nil62 if hasSystemParts {
63 out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction)
61 }64 }
62 }65 }
63 66
64 // contents67 // contents
65 contents := make([]client.Content, 0)68 if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
66 messagesResult := gjson.GetBytes(rawJSON, "messages")69 messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
67 if messagesResult.IsArray() {
68 messageResults := messagesResult.Array()
69 for i := 0; i < len(messageResults); i++ {
70 messageResult := messageResults[i]
71 roleResult := messageResult.Get("role")70 roleResult := messageResult.Get("role")
72 if roleResult.Type != gjson.String {71 if roleResult.Type != gjson.String {
73 continue72 return true
74 }73 }
75 role := roleResult.String()74 role := roleResult.String()
76 if role == "assistant" {75 if role == "assistant" {
77 role = "model"76 role = "model"
78 }77 }
79 clientContent := client.Content{Role: role, Parts: []client.Part{}}78
79 contentJSON := `{"role":"","parts":[]}`
80 contentJSON, _ = sjson.Set(contentJSON, "role", role)
81
80 contentsResult := messageResult.Get("content")82 contentsResult := messageResult.Get("content")
81 if contentsResult.IsArray() {83 if contentsResult.IsArray() {
82 contentResults := contentsResult.Array()84 contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
83 for j := 0; j < len(contentResults); j++ {85 switch contentResult.Get("type").String() {
84 contentResult := contentResults[j]86 case "text":
85 contentTypeResult := contentResult.Get("type")87 part := `{"text":""}`
86 if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {88 part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
87 prompt := contentResult.Get("text").String()89 contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
88 clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})90
89 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {91 case "tool_use":
90 functionName := contentResult.Get("name").String()92 functionName := contentResult.Get("name").String()
91 functionArgs := contentResult.Get("input").String()93 functionArgs := contentResult.Get("input").String()
92 var args map[string]any94 argsResult := gjson.Parse(functionArgs)
93 if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {95 if argsResult.IsObject() && gjson.Valid(functionArgs) {
94 clientContent.Parts = append(clientContent.Parts, client.Part{96 part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
95 FunctionCall: &client.FunctionCall{Name: functionName, Args: args},97 part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature)
96 ThoughtSignature: geminiCLIClaudeThoughtSignature,98 part, _ = sjson.Set(part, "functionCall.name", functionName)
97 })99 part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
100 contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
98 }101 }
99 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {102
103 case "tool_result":
100 toolCallID := contentResult.Get("tool_use_id").String()104 toolCallID := contentResult.Get("tool_use_id").String()
101 if toolCallID != "" {105 if toolCallID == "" {
102 funcName := toolCallID106 return true
103 toolCallIDs := strings.Split(toolCallID, "-")107 }
104 if len(toolCallIDs) > 1 {108 funcName := toolCallID
105 funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")109 toolCallIDs := strings.Split(toolCallID, "-")
106 }110 if len(toolCallIDs) > 1 {
107 responseData := contentResult.Get("content").Raw111 funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
108 functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
109 clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
110 }112 }
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)
111 }118 }
112 }119 return true
113 contents = append(contents, clientContent)120 })
121 out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
114 } else if contentsResult.Type == gjson.String {122 } else if contentsResult.Type == gjson.String {
115 prompt := contentsResult.String()123 part := `{"text":""}`
116 contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})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)
117 }127 }
118 }128 return true
129 })
119 }130 }
120 131
121 // tools132 // tools
122 var tools []client.ToolDeclaration133 if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
123 toolsResult := gjson.GetBytes(rawJSON, "tools")134 hasTools := false
124 if toolsResult.IsArray() {135 toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
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]
130 inputSchemaResult := toolResult.Get("input_schema")136 inputSchemaResult := toolResult.Get("input_schema")
131 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {137 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
132 inputSchema := inputSchemaResult.Raw138 inputSchema := inputSchemaResult.Raw
@@ -136,30 +142,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 3 unmodified lines
136 tool, _ = sjson.Delete(tool, "input_examples")142 tool, _ = sjson.Delete(tool, "input_examples")
137 tool, _ = sjson.Delete(tool, "type")143 tool, _ = sjson.Delete(tool, "type")
138 tool, _ = sjson.Delete(tool, "cache_control")144 tool, _ = sjson.Delete(tool, "cache_control")
139 var toolDeclaration any145 if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
140 if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {146 if !hasTools {
141 tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)147 out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`)
148 hasTools = true
149 }
150 out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool)
142 }151 }
143 }152 }
153 return true
154 })
155 if !hasTools {
156 out, _ = sjson.Delete(out, "request.tools")
144 }157 }
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))
163 }158 }
164 159
165 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled160 // 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
9import (9import (
10 "bytes"10 "bytes"
11 "context"11 "context"
12 "encoding/json"
13 "fmt"12 "fmt"
14 "strings"13 "strings"
15 "sync/atomic"14 "sync/atomic"
@@ -276,22 +275,16 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 260 unmodified lines
276 275
277 root := gjson.ParseBytes(rawJSON)276 root := gjson.ParseBytes(rawJSON)
278 277
279 response := map[string]interface{}{278 out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
280 "id": root.Get("response.responseId").String(),279 out, _ = sjson.Set(out, "id", root.Get("response.responseId").String())
281 "type": "message",280 out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String())
282 "role": "assistant",281
283 "model": root.Get("response.modelVersion").String(),282 inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
284 "content": []interface{}{},283 outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int()
285 "stop_reason": nil,284 out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
286 "stop_sequence": nil,285 out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
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 }
292 286
293 parts := root.Get("response.candidates.0.content.parts")287 parts := root.Get("response.candidates.0.content.parts")
294 var contentBlocks []interface{}
295 textBuilder := strings.Builder{}288 textBuilder := strings.Builder{}
296 thinkingBuilder := strings.Builder{}289 thinkingBuilder := strings.Builder{}
297 toolIDCounter := 0290 toolIDCounter := 0
@@ -301,10 +294,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 3 unmodified lines
301 if textBuilder.Len() == 0 {294 if textBuilder.Len() == 0 {
302 return295 return
303 }296 }
304 contentBlocks = append(contentBlocks, map[string]interface{}{297 block := `{"type":"text","text":""}`
305 "type": "text",298 block, _ = sjson.Set(block, "text", textBuilder.String())
306 "text": textBuilder.String(),299 out, _ = sjson.SetRaw(out, "content.-1", block)
307 })
308 textBuilder.Reset()300 textBuilder.Reset()
309 }301 }
310 302
@@ -312,10 +304,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 1 unmodified line
312 if thinkingBuilder.Len() == 0 {304 if thinkingBuilder.Len() == 0 {
313 return305 return
314 }306 }
315 contentBlocks = append(contentBlocks, map[string]interface{}{307 block := `{"type":"thinking","thinking":""}`
316 "type": "thinking",308 block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
317 "thinking": thinkingBuilder.String(),309 out, _ = sjson.SetRaw(out, "content.-1", block)
318 })
319 thinkingBuilder.Reset()310 thinkingBuilder.Reset()
320 }311 }
321 312
@@ -339,21 +330,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 17 unmodified lines
339 330
340 name := functionCall.Get("name").String()331 name := functionCall.Get("name").String()
341 toolIDCounter++332 toolIDCounter++
342 toolBlock := map[string]interface{}{333 toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
343 "type": "tool_use",334 toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
344 "id": fmt.Sprintf("tool_%d", toolIDCounter),335 toolBlock, _ = sjson.Set(toolBlock, "name", name)
345 "name": name,336 inputRaw := "{}"
346 "input": map[string]interface{}{},337 if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
347 }338 inputRaw = args.Raw
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 }
354 }339 }
355 340 toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
356 contentBlocks = append(contentBlocks, toolBlock)341 out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
357 continue342 continue
358 }343 }
359 }344 }
@@ -362,8 +347,6 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 2 unmodified lines
362 flushThinking()347 flushThinking()
363 flushText()348 flushText()
364 349
365 response["content"] = contentBlocks
366
367 stopReason := "end_turn"350 stopReason := "end_turn"
368 if hasToolCall {351 if hasToolCall {
369 stopReason = "tool_use"352 stopReason = "tool_use"
@@ -379,19 +362,13 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 9 unmodified lines
379 }362 }
380 }363 }
381 }364 }
382 response["stop_reason"] = stopReason365 out, _ = sjson.Set(out, "stop_reason", stopReason)
383 366
384 if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {367 if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() {
385 if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() {368 out, _ = sjson.Delete(out, "usage")
386 delete(response, "usage")
387 }
388 }369 }
389 370
390 encoded, err := json.Marshal(response)371 return out
391 if err != nil {
392 return ""
393 }
394 return string(encoded)
395}372}
396 373
397func ClaudeTokenCount(ctx context.Context, count int64) string {374func 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
7 7
8import (8import (
9 "bytes"9 "bytes"
10 "encoding/json"
11 "fmt"10 "fmt"
12 11
13 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"12 "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
117 116
118// FunctionCallGroup represents a group of function calls and their responses117// FunctionCallGroup represents a group of function calls and their responses
119type FunctionCallGroup struct {118type FunctionCallGroup struct {
120 ModelContent map[string]interface{}
121 FunctionCalls []gjson.Result
122 ResponsesNeeded int119 ResponsesNeeded int
123}120}
124 121
@@ -146,7 +143,7 @@ func fixCLIToolResponse(input string) (string, error) { · 21 unmodified lines
146 }143 }
147 144
148 // Initialize data structures for processing and grouping145 // Initialize data structures for processing and grouping
149 var newContents []interface{} // Final processed contents array146 contentsWrapper := `{"contents":[]}`
150 var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses147 var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
151 var collectedResponses []gjson.Result // Standalone responses to be matched148 var collectedResponses []gjson.Result // Standalone responses to be matched
152 149
@@ -178,23 +175,17 @@ func fixCLIToolResponse(input string) (string, error) { · 25 unmodified lines
178 collectedResponses = collectedResponses[group.ResponsesNeeded:]175 collectedResponses = collectedResponses[group.ResponsesNeeded:]
179 176
180 // Create merged function response content177 // Create merged function response content
181 var responseParts []interface{}178 functionResponseContent := `{"parts":[],"role":"function"}`
182 for _, response := range groupResponses {179 for _, response := range groupResponses {
183 var responseMap map[string]interface{}180 if !response.IsObject() {
184 errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)181 log.Warnf("failed to parse function response")
185 if errUnmarshal != nil {
186 log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
187 continue182 continue
188 }183 }
189 responseParts = append(responseParts, responseMap)184 functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
190 }185 }
191 186
192 if len(responseParts) > 0 {187 if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
193 functionResponseContent := map[string]interface{}{188 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
194 "parts": responseParts,
195 "role": "function",
196 }
197 newContents = append(newContents, functionResponseContent)
198 }189 }
199 190
200 // Remove this group as it's been satisfied191 // Remove this group as it's been satisfied
@@ -208,50 +199,42 @@ func fixCLIToolResponse(input string) (string, error) { · 7 unmodified lines
208 199
209 // If this is a model with function calls, create a new group200 // If this is a model with function calls, create a new group
210 if role == "model" {201 if role == "model" {
211 var functionCallsInThisModel []gjson.Result202 functionCallsCount := 0
212 parts.ForEach(func(_, part gjson.Result) bool {203 parts.ForEach(func(_, part gjson.Result) bool {
213 if part.Get("functionCall").Exists() {204 if part.Get("functionCall").Exists() {
214 functionCallsInThisModel = append(functionCallsInThisModel, part)205 functionCallsCount++
215 }206 }
216 return true207 return true
217 })208 })
218 209
219 if len(functionCallsInThisModel) > 0 {210 if functionCallsCount > 0 {
220 // Add the model content211 // Add the model content
221 var contentMap map[string]interface{}212 if !value.IsObject() {
222 errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)213 log.Warnf("failed to parse model content")
223 if errUnmarshal != nil {
224 log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal)
225 return true214 return true
226 }215 }
227 newContents = append(newContents, contentMap)216 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
228 217
229 // Create a new group for tracking responses218 // Create a new group for tracking responses
230 group := &FunctionCallGroup{219 group := &FunctionCallGroup{
231 ModelContent: contentMap,220 ResponsesNeeded: functionCallsCount,
232 FunctionCalls: functionCallsInThisModel,
233 ResponsesNeeded: len(functionCallsInThisModel),
234 }221 }
235 pendingGroups = append(pendingGroups, group)222 pendingGroups = append(pendingGroups, group)
236 } else {223 } else {
237 // Regular model content without function calls224 // Regular model content without function calls
238 var contentMap map[string]interface{}225 if !value.IsObject() {
239 errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)226 log.Warnf("failed to parse content")
240 if errUnmarshal != nil {
241 log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
242 return true227 return true
243 }228 }
244 newContents = append(newContents, contentMap)229 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
245 }230 }
246 } else {231 } else {
247 // Non-model content (user, etc.)232 // Non-model content (user, etc.)
248 var contentMap map[string]interface{}233 if !value.IsObject() {
249 errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap)234 log.Warnf("failed to parse content")
250 if errUnmarshal != nil {
251 log.Warnf("failed to unmarshal content: %v\n", errUnmarshal)
252 return true235 return true
253 }236 }
254 newContents = append(newContents, contentMap)237 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
255 }238 }
256 239
257 return true240 return true
@@ -263,31 +246,24 @@ func fixCLIToolResponse(input string) (string, error) { · 5 unmodified lines
263 groupResponses := collectedResponses[:group.ResponsesNeeded]246 groupResponses := collectedResponses[:group.ResponsesNeeded]
264 collectedResponses = collectedResponses[group.ResponsesNeeded:]247 collectedResponses = collectedResponses[group.ResponsesNeeded:]
265 248
266 var responseParts []interface{}249 functionResponseContent := `{"parts":[],"role":"function"}`
267 for _, response := range groupResponses {250 for _, response := range groupResponses {
268 var responseMap map[string]interface{}251 if !response.IsObject() {
269 errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap)252 log.Warnf("failed to parse function response")
270 if errUnmarshal != nil {
271 log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal)
272 continue253 continue
273 }254 }
274 responseParts = append(responseParts, responseMap)255 functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", response.Raw)
275 }256 }
276 257
277 if len(responseParts) > 0 {258 if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
278 functionResponseContent := map[string]interface{}{259 contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
279 "parts": responseParts,
280 "role": "function",
281 }
282 newContents = append(newContents, functionResponseContent)
283 }260 }
284 }261 }
285 }262 }
286 263
287 // Update the original JSON with the new contents264 // Update the original JSON with the new contents
288 result := input265 result := input
289 newContentsJSON, _ := json.Marshal(newContents)266 result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
290 result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON))
291 267
292 return result, nil268 return result, nil
293}269}
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
278 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)278 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
279 continue279 continue
280 }280 }
281 fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})281 fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
282 if errSet != nil {282 if errSet != nil {
283 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)283 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
284 continue284 continue
@@ -293,7 +293,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 8 unmodified lines
293 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)293 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
294 continue294 continue
295 }295 }
296 fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})296 fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
297 if errSet != nil {297 if errSet != nil {
298 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)298 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
299 continue299 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
8import (8import (
9 "bytes"9 "bytes"
10 "context"10 "context"
11 "encoding/json"
12 "fmt"11 "fmt"
13 "strings"12 "strings"
14 "sync/atomic"13 "sync/atomic"
@@ -171,21 +170,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ · 156 unmodified lines
171 mimeType = "image/png"170 mimeType = "image/png"
172 }171 }
173 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)172 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
174 imagePayload, err := json.Marshal(map[string]any{173 imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
175 "type": "image_url",174 imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
176 "image_url": map[string]string{
177 "url": imageURL,
178 },
179 })
180 if err != nil {
181 continue
182 }
183 imagesResult := gjson.Get(template, "choices.0.delta.images")175 imagesResult := gjson.Get(template, "choices.0.delta.images")
184 if !imagesResult.Exists() || !imagesResult.IsArray() {176 if !imagesResult.Exists() || !imagesResult.IsArray() {
185 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)177 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
186 }178 }
187 template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")179 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)
189 }181 }
190 }182 }
191 }183 }
internal/translator/gemini/claude/gemini_claude_request.go+78−83
@@ -7,10 +7,8 @@ package claude · 6 unmodified lines
7 7
8import (8import (
9 "bytes"9 "bytes"
10 "encoding/json"
11 "strings"10 "strings"
12 11
13 client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces"
14 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"12 "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common"
15 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"13 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
16 "github.com/tidwall/gjson"14 "github.com/tidwall/gjson"
@@ -34,92 +32,100 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 17 unmodified lines
34 rawJSON := bytes.Clone(inputRawJSON)32 rawJSON := bytes.Clone(inputRawJSON)
35 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)33 rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
36 34
35 // Build output Gemini CLI request JSON
36 out := `{"contents":[]}`
37 out, _ = sjson.Set(out, "model", modelName)
38
37 // system instruction39 // system instruction
38 var systemInstruction *client.Content40 if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
39 systemResult := gjson.GetBytes(rawJSON, "system")41 systemInstruction := `{"role":"user","parts":[]}`
40 if systemResult.IsArray() {42 hasSystemParts := false
41 systemResults := systemResult.Array()43 systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
42 systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}}44 if systemPromptResult.Get("type").String() == "text" {
43 for i := 0; i < len(systemResults); i++ {45 textResult := systemPromptResult.Get("text")
44 systemPromptResult := systemResults[i]46 if textResult.Type == gjson.String {
45 systemTypePromptResult := systemPromptResult.Get("type")47 part := `{"text":""}`
46 if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {48 part, _ = sjson.Set(part, "text", textResult.String())
47 systemPrompt := systemPromptResult.Get("text").String()49 systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
48 systemPart := client.Part{Text: systemPrompt}50 hasSystemParts = true
49 systemInstruction.Parts = append(systemInstruction.Parts, systemPart)51 }
50 }52 }
51 }53 return true
52 if len(systemInstruction.Parts) == 0 {54 })
53 systemInstruction = nil55 if hasSystemParts {
56 out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction)
54 }57 }
55 }58 }
56 59
57 // contents60 // contents
58 contents := make([]client.Content, 0)61 if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() {
59 messagesResult := gjson.GetBytes(rawJSON, "messages")62 messagesResult.ForEach(func(_, messageResult gjson.Result) bool {
60 if messagesResult.IsArray() {
61 messageResults := messagesResult.Array()
62 for i := 0; i < len(messageResults); i++ {
63 messageResult := messageResults[i]
64 roleResult := messageResult.Get("role")63 roleResult := messageResult.Get("role")
65 if roleResult.Type != gjson.String {64 if roleResult.Type != gjson.String {
66 continue65 return true
67 }66 }
68 role := roleResult.String()67 role := roleResult.String()
69 if role == "assistant" {68 if role == "assistant" {
70 role = "model"69 role = "model"
71 }70 }
72 clientContent := client.Content{Role: role, Parts: []client.Part{}}71
72 contentJSON := `{"role":"","parts":[]}`
73 contentJSON, _ = sjson.Set(contentJSON, "role", role)
74
73 contentsResult := messageResult.Get("content")75 contentsResult := messageResult.Get("content")
74 if contentsResult.IsArray() {76 if contentsResult.IsArray() {
75 contentResults := contentsResult.Array()77 contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
76 for j := 0; j < len(contentResults); j++ {78 switch contentResult.Get("type").String() {
77 contentResult := contentResults[j]79 case "text":
78 contentTypeResult := contentResult.Get("type")80 part := `{"text":""}`
79 if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {81 part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
80 prompt := contentResult.Get("text").String()82 contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
81 clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt})83
82 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {84 case "tool_use":
83 functionName := contentResult.Get("name").String()85 functionName := contentResult.Get("name").String()
84 functionArgs := contentResult.Get("input").String()86 functionArgs := contentResult.Get("input").String()
85 var args map[string]any87 argsResult := gjson.Parse(functionArgs)
86 if err := json.Unmarshal([]byte(functionArgs), &args); err == nil {88 if argsResult.IsObject() && gjson.Valid(functionArgs) {
87 clientContent.Parts = append(clientContent.Parts, client.Part{89 part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
88 FunctionCall: &client.FunctionCall{Name: functionName, Args: args},90 part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature)
89 ThoughtSignature: geminiClaudeThoughtSignature,91 part, _ = sjson.Set(part, "functionCall.name", functionName)
90 })92 part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
93 contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
91 }94 }
92 } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {95
96 case "tool_result":
93 toolCallID := contentResult.Get("tool_use_id").String()97 toolCallID := contentResult.Get("tool_use_id").String()
94 if toolCallID != "" {98 if toolCallID == "" {
95 funcName := toolCallID99 return true
96 toolCallIDs := strings.Split(toolCallID, "-")100 }
97 if len(toolCallIDs) > 1 {101 funcName := toolCallID
98 funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")102 toolCallIDs := strings.Split(toolCallID, "-")
99 }103 if len(toolCallIDs) > 1 {
100 responseData := contentResult.Get("content").Raw104 funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
101 functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}}
102 clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse})
103 }105 }
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)
104 }111 }
105 }112 return true
106 contents = append(contents, clientContent)113 })
114 out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
107 } else if contentsResult.Type == gjson.String {115 } else if contentsResult.Type == gjson.String {
108 prompt := contentsResult.String()116 part := `{"text":""}`
109 contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}})117 part, _ = sjson.Set(part, "text", contentsResult.String())
118 contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
119 out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
110 }120 }
111 }121 return true
122 })
112 }123 }
113 124
114 // tools125 // tools
115 var tools []client.ToolDeclaration126 if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() {
116 toolsResult := gjson.GetBytes(rawJSON, "tools")127 hasTools := false
117 if toolsResult.IsArray() {128 toolsResult.ForEach(func(_, toolResult gjson.Result) bool {
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]
123 inputSchemaResult := toolResult.Get("input_schema")129 inputSchemaResult := toolResult.Get("input_schema")
124 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {130 if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
125 inputSchema := inputSchemaResult.Raw131 inputSchema := inputSchemaResult.Raw
@@ -129,30 +135,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 3 unmodified lines
129 tool, _ = sjson.Delete(tool, "input_examples")135 tool, _ = sjson.Delete(tool, "input_examples")
130 tool, _ = sjson.Delete(tool, "type")136 tool, _ = sjson.Delete(tool, "type")
131 tool, _ = sjson.Delete(tool, "cache_control")137 tool, _ = sjson.Delete(tool, "cache_control")
132 var toolDeclaration any138 if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
133 if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil {139 if !hasTools {
134 tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration)140 out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`)
141 hasTools = true
142 }
143 out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool)
135 }144 }
136 }145 }
146 return true
147 })
148 if !hasTools {
149 out, _ = sjson.Delete(out, "tools")
137 }150 }
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))
156 }151 }
157 152
158 // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled153 // 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
9import (9import (
10 "bytes"10 "bytes"
11 "context"11 "context"
12 "encoding/json"
13 "fmt"12 "fmt"
14 "strings"13 "strings"
15 "sync/atomic"14 "sync/atomic"
@@ -282,22 +281,16 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 266 unmodified lines
282 281
283 root := gjson.ParseBytes(rawJSON)282 root := gjson.ParseBytes(rawJSON)
284 283
285 response := map[string]interface{}{284 out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
286 "id": root.Get("responseId").String(),285 out, _ = sjson.Set(out, "id", root.Get("responseId").String())
287 "type": "message",286 out, _ = sjson.Set(out, "model", root.Get("modelVersion").String())
288 "role": "assistant",287
289 "model": root.Get("modelVersion").String(),288 inputTokens := root.Get("usageMetadata.promptTokenCount").Int()
290 "content": []interface{}{},289 outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int()
291 "stop_reason": nil,290 out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
292 "stop_sequence": nil,291 out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
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 }
298 292
299 parts := root.Get("candidates.0.content.parts")293 parts := root.Get("candidates.0.content.parts")
300 var contentBlocks []interface{}
301 textBuilder := strings.Builder{}294 textBuilder := strings.Builder{}
302 thinkingBuilder := strings.Builder{}295 thinkingBuilder := strings.Builder{}
303 toolIDCounter := 0296 toolIDCounter := 0
@@ -307,10 +300,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 3 unmodified lines
307 if textBuilder.Len() == 0 {300 if textBuilder.Len() == 0 {
308 return301 return
309 }302 }
310 contentBlocks = append(contentBlocks, map[string]interface{}{303 block := `{"type":"text","text":""}`
311 "type": "text",304 block, _ = sjson.Set(block, "text", textBuilder.String())
312 "text": textBuilder.String(),305 out, _ = sjson.SetRaw(out, "content.-1", block)
313 })
314 textBuilder.Reset()306 textBuilder.Reset()
315 }307 }
316 308
@@ -318,10 +310,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 1 unmodified line
318 if thinkingBuilder.Len() == 0 {310 if thinkingBuilder.Len() == 0 {
319 return311 return
320 }312 }
321 contentBlocks = append(contentBlocks, map[string]interface{}{313 block := `{"type":"thinking","thinking":""}`
322 "type": "thinking",314 block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
323 "thinking": thinkingBuilder.String(),315 out, _ = sjson.SetRaw(out, "content.-1", block)
324 })
325 thinkingBuilder.Reset()316 thinkingBuilder.Reset()
326 }317 }
327 318
@@ -345,21 +336,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 17 unmodified lines
345 336
346 name := functionCall.Get("name").String()337 name := functionCall.Get("name").String()
347 toolIDCounter++338 toolIDCounter++
348 toolBlock := map[string]interface{}{339 toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
349 "type": "tool_use",340 toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
350 "id": fmt.Sprintf("tool_%d", toolIDCounter),341 toolBlock, _ = sjson.Set(toolBlock, "name", name)
351 "name": name,342 inputRaw := "{}"
352 "input": map[string]interface{}{},343 if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
353 }344 inputRaw = args.Raw
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 }
360 }345 }
361 346 toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
362 contentBlocks = append(contentBlocks, toolBlock)347 out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
363 continue348 continue
364 }349 }
365 }350 }
@@ -368,8 +353,6 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 2 unmodified lines
368 flushThinking()353 flushThinking()
369 flushText()354 flushText()
370 355
371 response["content"] = contentBlocks
372
373 stopReason := "end_turn"356 stopReason := "end_turn"
374 if hasToolCall {357 if hasToolCall {
375 stopReason = "tool_use"358 stopReason = "tool_use"
@@ -385,19 +368,13 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 9 unmodified lines
385 }368 }
386 }369 }
387 }370 }
388 response["stop_reason"] = stopReason371 out, _ = sjson.Set(out, "stop_reason", stopReason)
389 372
390 if usage := response["usage"].(map[string]interface{}); usage["input_tokens"] == int64(0) && usage["output_tokens"] == int64(0) {373 if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() {
391 if usageMeta := root.Get("usageMetadata"); !usageMeta.Exists() {374 out, _ = sjson.Delete(out, "usage")
392 delete(response, "usage")
393 }
394 }375 }
395 376
396 encoded, err := json.Marshal(response)377 return out
397 if err != nil {
398 return ""
399 }
400 return string(encoded)
401}378}
402 379
403func ClaudeTokenCount(ctx context.Context, count int64) string {380func 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
320 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)320 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
321 continue321 continue
322 }322 }
323 fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})323 fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
324 if errSet != nil {324 if errSet != nil {
325 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)325 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
326 continue326 continue
@@ -335,7 +335,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 8 unmodified lines
335 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)335 log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
336 continue336 continue
337 }337 }
338 fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{})338 fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
339 if errSet != nil {339 if errSet != nil {
340 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)340 log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
341 continue341 continue
internal/translator/gemini/openai/chat-completions/gemini_openai_response.go+6−21
@@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines
8import (8import (
9 "bytes"9 "bytes"
10 "context"10 "context"
11 "encoding/json"
12 "fmt"11 "fmt"
13 "strings"12 "strings"
14 "sync/atomic"13 "sync/atomic"
@@ -173,21 +172,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR · 158 unmodified lines
173 mimeType = "image/png"172 mimeType = "image/png"
174 }173 }
175 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)174 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
176 imagePayload, err := json.Marshal(map[string]any{175 imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
177 "type": "image_url",176 imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
178 "image_url": map[string]string{
179 "url": imageURL,
180 },
181 })
182 if err != nil {
183 continue
184 }
185 imagesResult := gjson.Get(template, "choices.0.delta.images")177 imagesResult := gjson.Get(template, "choices.0.delta.images")
186 if !imagesResult.Exists() || !imagesResult.IsArray() {178 if !imagesResult.Exists() || !imagesResult.IsArray() {
187 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)179 template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`)
188 }180 }
189 template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")181 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)
191 }183 }
192 }184 }
193 }185 }
@@ -305,21 +297,14 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina · 111 unmodified lines
305 mimeType = "image/png"297 mimeType = "image/png"
306 }298 }
307 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)299 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
308 imagePayload, err := json.Marshal(map[string]any{300 imagePayload := `{"image_url":{"url":""},"type":"image_url"}`
309 "type": "image_url",301 imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
310 "image_url": map[string]string{
311 "url": imageURL,
312 },
313 })
314 if err != nil {
315 continue
316 }
317 imagesResult := gjson.Get(template, "choices.0.message.images")302 imagesResult := gjson.Get(template, "choices.0.message.images")
318 if !imagesResult.Exists() || !imagesResult.IsArray() {303 if !imagesResult.Exists() || !imagesResult.IsArray() {
319 template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)304 template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`)
320 }305 }
321 template, _ = sjson.Set(template, "choices.0.message.role", "assistant")306 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)
323 }308 }
324 }309 }
325 }310 }
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
377 }377 }
378 378
379 // Compose outputs in encountered order: reasoning, message, function_calls379 // Compose outputs in encountered order: reasoning, message, function_calls
380 var outputs []interface{}380 outputsWrapper := `{"arr":[]}`
381 if st.ReasoningOpened {381 if st.ReasoningOpened {
382 outputs = append(outputs, map[string]interface{}{382 item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
383 "id": st.ReasoningItemID,383 item, _ = sjson.Set(item, "id", st.ReasoningItemID)
384 "type": "reasoning",384 item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
385 "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}},385 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
386 })
387 }386 }
388 if st.MsgOpened {387 if st.MsgOpened {
389 outputs = append(outputs, map[string]interface{}{388 item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
390 "id": st.CurrentMsgID,389 item, _ = sjson.Set(item, "id", st.CurrentMsgID)
391 "type": "message",390 item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
392 "status": "completed",391 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
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 })
401 }392 }
402 if len(st.FuncArgsBuf) > 0 {393 if len(st.FuncArgsBuf) > 0 {
403 idxs := make([]int, 0, len(st.FuncArgsBuf))394 idxs := make([]int, 0, len(st.FuncArgsBuf))
@@ -416,18 +407,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, · 12 unmodified lines
416 if b := st.FuncArgsBuf[idx]; b != nil {407 if b := st.FuncArgsBuf[idx]; b != nil {
417 args = b.String()408 args = b.String()
418 }409 }
419 outputs = append(outputs, map[string]interface{}{410 item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
420 "id": fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]),411 item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
421 "type": "function_call",412 item, _ = sjson.Set(item, "arguments", args)
422 "status": "completed",413 item, _ = sjson.Set(item, "call_id", st.FuncCallIDs[idx])
423 "arguments": args,414 item, _ = sjson.Set(item, "name", st.FuncNames[idx])
424 "call_id": st.FuncCallIDs[idx],415 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
425 "name": st.FuncNames[idx],
426 })
427 }416 }
428 }417 }
429 if len(outputs) > 0 {418 if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
430 completed, _ = sjson.Set(completed, "response.output", outputs)419 completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
431 }420 }
432 421
433 // usage mapping422 // usage mapping
@@ -558,11 +547,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 124 unmodified lines
558 }547 }
559 548
560 // Build outputs from candidates[0].content.parts549 // Build outputs from candidates[0].content.parts
561 var outputs []interface{}
562 var reasoningText strings.Builder550 var reasoningText strings.Builder
563 var reasoningEncrypted string551 var reasoningEncrypted string
564 var messageText strings.Builder552 var messageText strings.Builder
565 var haveMessage bool553 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
566 if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() {568 if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() {
567 parts.ForEach(func(_, p gjson.Result) bool {569 parts.ForEach(func(_, p gjson.Result) bool {
568 if p.Get("thought").Bool() {570 if p.Get("thought").Bool() {
@@ -583,19 +585,16 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 14 unmodified lines
583 name := fc.Get("name").String()585 name := fc.Get("name").String()
584 args := fc.Get("args")586 args := fc.Get("args")
585 callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))587 callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
586 outputs = append(outputs, map[string]interface{}{588 itemJSON := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
587 "id": fmt.Sprintf("fc_%s", callID),589 itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID))
588 "type": "function_call",590 itemJSON, _ = sjson.Set(itemJSON, "call_id", callID)
589 "status": "completed",591 itemJSON, _ = sjson.Set(itemJSON, "name", name)
590 "arguments": func() string {592 argsStr := ""
591 if args.Exists() {593 if args.Exists() {
592 return args.Raw594 argsStr = args.Raw
593 }595 }
594 return ""596 itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr)
595 }(),597 appendOutput(itemJSON)
596 "call_id": callID,
597 "name": name,
598 })
599 return true598 return true
600 }599 }
601 return true600 return true
@@ -605,42 +604,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 3 unmodified lines
605 // Reasoning output item604 // Reasoning output item
606 if reasoningText.Len() > 0 || reasoningEncrypted != "" {605 if reasoningText.Len() > 0 || reasoningEncrypted != "" {
607 rid := strings.TrimPrefix(id, "resp_")606 rid := strings.TrimPrefix(id, "resp_")
608 item := map[string]interface{}{607 itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}`
609 "id": fmt.Sprintf("rs_%s", rid),608 itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid))
610 "type": "reasoning",609 itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted)
611 "encrypted_content": reasoningEncrypted,
612 }
613 var summaries []interface{}
614 if reasoningText.Len() > 0 {610 if reasoningText.Len() > 0 {
615 summaries = append(summaries, map[string]interface{}{611 summaryJSON := `{"type":"summary_text","text":""}`
616 "type": "summary_text",612 summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String())
617 "text": reasoningText.String(),613 itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]")
618 })614 itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON)
619 }
620 if summaries != nil {
621 item["summary"] = summaries
622 }615 }
623 outputs = append(outputs, item)616 appendOutput(itemJSON)
624 }617 }
625 618
626 // Assistant message output item619 // Assistant message output item
627 if haveMessage {620 if haveMessage {
628 outputs = append(outputs, map[string]interface{}{621 itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
629 "id": fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")),622 itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")))
630 "type": "message",623 itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String())
631 "status": "completed",624 appendOutput(itemJSON)
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)
644 }625 }
645 626
646 // usage mapping627 // usage mapping
internal/translator/openai/claude/openai_claude_request.go+2−8
@@ -7,7 +7,6 @@ package claude · 6 unmodified lines
7 7
8import (8import (
9 "bytes"9 "bytes"
10 "encoding/json"
11 "strings"10 "strings"
12 11
13 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"12 "github.com/router-for-me/CLIProxyAPI/v6/internal/util"
@@ -138,11 +137,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 124 unmodified lines
138 137
139 // Convert input to arguments JSON string138 // Convert input to arguments JSON string
140 if input := part.Get("input"); input.Exists() {139 if input := part.Get("input"); input.Exists() {
141 if inputJSON, err := json.Marshal(input.Value()); err == nil {140 toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw)
142 toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", string(inputJSON))
143 } else {
144 toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
145 }
146 } else {141 } else {
147 toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")142 toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}")
148 }143 }
@@ -191,8 +186,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 42 unmodified lines
191 // Emit tool calls in a separate assistant message186 // Emit tool calls in a separate assistant message
192 if role == "assistant" && len(toolCalls) > 0 {187 if role == "assistant" && len(toolCalls) > 0 {
193 toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`188 toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}`
194 toolCallsJSON, _ := json.Marshal(toolCalls)189 toolCallMsgJSON, _ = sjson.Set(toolCallMsgJSON, "tool_calls", toolCalls)
195 toolCallMsgJSON, _ = sjson.SetRaw(toolCallMsgJSON, "tool_calls", string(toolCallsJSON))
196 messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())190 messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value())
197 }191 }
198 192
internal/translator/openai/claude/openai_claude_response.go+136−307
@@ -8,7 +8,6 @@ package claude · 7 unmodified lines
8import (8import (
9 "bytes"9 "bytes"
10 "context"10 "context"
11 "encoding/json"
12 "fmt"11 "fmt"
13 "strings"12 "strings"
14 13
@@ -133,24 +132,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 118 unmodified lines
133 if delta := root.Get("choices.0.delta"); delta.Exists() {132 if delta := root.Get("choices.0.delta"); delta.Exists() {
134 if !param.MessageStarted {133 if !param.MessageStarted {
135 // Send message_start event134 // Send message_start event
136 messageStart := map[string]interface{}{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}}}`
137 "type": "message_start",136 messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID)
138 "message": map[string]interface{}{137 messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model)
139 "id": param.MessageID,138 results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n")
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")
154 param.MessageStarted = true139 param.MessageStarted = true
155 140
156 // Don't send content_block_start for text here - wait for actual content141 // 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
168 param.ThinkingContentBlockIndex = param.NextContentBlockIndex153 param.ThinkingContentBlockIndex = param.NextContentBlockIndex
169 param.NextContentBlockIndex++154 param.NextContentBlockIndex++
170 }155 }
171 contentBlockStart := map[string]interface{}{156 contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
172 "type": "content_block_start",157 contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex)
173 "index": param.ThinkingContentBlockIndex,158 results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
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")
181 param.ThinkingContentBlockStarted = true159 param.ThinkingContentBlockStarted = true
182 }160 }
183 161
184 thinkingDelta := map[string]interface{}{162 thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
185 "type": "content_block_delta",163 thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex)
186 "index": param.ThinkingContentBlockIndex,164 thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText)
187 "delta": map[string]interface{}{165 results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n")
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")
194 }166 }
195 }167 }
196 168
@@ -203,29 +175,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 6 unmodified lines
203 param.TextContentBlockIndex = param.NextContentBlockIndex175 param.TextContentBlockIndex = param.NextContentBlockIndex
204 param.NextContentBlockIndex++176 param.NextContentBlockIndex++
205 }177 }
206 contentBlockStart := map[string]interface{}{178 contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
207 "type": "content_block_start",179 contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex)
208 "index": param.TextContentBlockIndex,180 results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
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")
216 param.TextContentBlockStarted = true181 param.TextContentBlockStarted = true
217 }182 }
218 183
219 contentDelta := map[string]interface{}{184 contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
220 "type": "content_block_delta",185 contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex)
221 "index": param.TextContentBlockIndex,186 contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String())
222 "delta": map[string]interface{}{187 results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n")
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")
229 188
230 // Accumulate content189 // Accumulate content
231 param.ContentAccumulator.WriteString(content.String())190 param.ContentAccumulator.WriteString(content.String())
@@ -263,18 +222,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 31 unmodified lines
263 stopTextContentBlock(param, &results)222 stopTextContentBlock(param, &results)
264 223
265 // Send content_block_start for tool_use224 // Send content_block_start for tool_use
266 contentBlockStart := map[string]interface{}{225 contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
267 "type": "content_block_start",226 contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
268 "index": blockIndex,227 contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", accumulator.ID)
269 "content_block": map[string]interface{}{228 contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
270 "type": "tool_use",229 results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
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")
278 }230 }
279 231
280 // Handle function arguments232 // Handle function arguments
@@ -298,12 +250,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 17 unmodified lines
298 250
299 // Send content_block_stop for thinking content if needed251 // Send content_block_stop for thinking content if needed
300 if param.ThinkingContentBlockStarted {252 if param.ThinkingContentBlockStarted {
301 contentBlockStop := map[string]interface{}{253 contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
302 "type": "content_block_stop",254 contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
303 "index": param.ThinkingContentBlockIndex,255 results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
304 }
305 contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
306 results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
307 param.ThinkingContentBlockStarted = false256 param.ThinkingContentBlockStarted = false
308 param.ThinkingContentBlockIndex = -1257 param.ThinkingContentBlockIndex = -1
309 }258 }
@@ -319,24 +268,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 9 unmodified lines
319 268
320 // Send complete input_json_delta with all accumulated arguments269 // Send complete input_json_delta with all accumulated arguments
321 if accumulator.Arguments.Len() > 0 {270 if accumulator.Arguments.Len() > 0 {
322 inputDelta := map[string]interface{}{271 inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
323 "type": "content_block_delta",272 inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
324 "index": blockIndex,273 inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
325 "delta": map[string]interface{}{274 results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
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")
332 }275 }
333 276
334 contentBlockStop := map[string]interface{}{277 contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
335 "type": "content_block_stop",278 contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
336 "index": blockIndex,279 results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
337 }
338 contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
339 results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
340 delete(param.ToolCallBlockIndexes, index)280 delete(param.ToolCallBlockIndexes, index)
341 }281 }
342 param.ContentBlocksStopped = true282 param.ContentBlocksStopped = true
@@ -361,20 +301,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 18 unmodified lines
361 }301 }
362 }302 }
363 // Send message_delta with usage303 // Send message_delta with usage
364 messageDelta := map[string]interface{}{304 messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
365 "type": "message_delta",305 messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
366 "delta": map[string]interface{}{306 messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
367 "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason),307 messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
368 "stop_sequence": nil,308 results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
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")
378 param.MessageDeltaSent = true309 param.MessageDeltaSent = true
379 310
380 emitMessageStopIfNeeded(param, &results)311 emitMessageStopIfNeeded(param, &results)
@@ -390,12 +321,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 9 unmodified lines
390 321
391 // Ensure all content blocks are stopped before final events322 // Ensure all content blocks are stopped before final events
392 if param.ThinkingContentBlockStarted {323 if param.ThinkingContentBlockStarted {
393 contentBlockStop := map[string]interface{}{324 contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
394 "type": "content_block_stop",325 contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
395 "index": param.ThinkingContentBlockIndex,326 results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
396 }
397 contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
398 results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
399 param.ThinkingContentBlockStarted = false327 param.ThinkingContentBlockStarted = false
400 param.ThinkingContentBlockIndex = -1328 param.ThinkingContentBlockIndex = -1
401 }329 }
@@ -408,24 +336,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 6 unmodified lines
408 blockIndex := param.toolContentBlockIndex(index)336 blockIndex := param.toolContentBlockIndex(index)
409 337
410 if accumulator.Arguments.Len() > 0 {338 if accumulator.Arguments.Len() > 0 {
411 inputDelta := map[string]interface{}{339 inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
412 "type": "content_block_delta",340 inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
413 "index": blockIndex,341 inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
414 "delta": map[string]interface{}{342 results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
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")
421 }343 }
422 344
423 contentBlockStop := map[string]interface{}{345 contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
424 "type": "content_block_stop",346 contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
425 "index": blockIndex,347 results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
426 }
427 contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
428 results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
429 delete(param.ToolCallBlockIndexes, index)348 delete(param.ToolCallBlockIndexes, index)
430 }349 }
431 param.ContentBlocksStopped = true350 param.ContentBlocksStopped = true
@@ -433,16 +352,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 1 unmodified line
433 352
434 // If we haven't sent message_delta yet (no usage info was received), send it now353 // If we haven't sent message_delta yet (no usage info was received), send it now
435 if param.FinishReason != "" && !param.MessageDeltaSent {354 if param.FinishReason != "" && !param.MessageDeltaSent {
436 messageDelta := map[string]interface{}{355 messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null}}`
437 "type": "message_delta",356 messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason))
438 "delta": map[string]interface{}{357 results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
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")
446 param.MessageDeltaSent = true358 param.MessageDeltaSent = true
447 }359 }
448 360
@@ -455,105 +367,73 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 6 unmodified lines
455func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {367func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
456 root := gjson.ParseBytes(rawJSON)368 root := gjson.ParseBytes(rawJSON)
457 369
458 // Build Anthropic response370 out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
459 response := map[string]interface{}{371 out, _ = sjson.Set(out, "id", root.Get("id").String())
460 "id": root.Get("id").String(),372 out, _ = sjson.Set(out, "model", root.Get("model").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 }
472 373
473 // Process message content and tool calls374 // Process message content and tool calls
474 var contentBlocks []interface{}375 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
475
476 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
477 choice := choices.Array()[0] // Take first choice376 choice := choices.Array()[0] // Take first choice
478 reasoningNode := choice.Get("message.reasoning_content")
479 allReasoning := collectOpenAIReasoningTexts(reasoningNode)
480 377
481 for _, reasoningText := range allReasoning {378 reasoningNode := choice.Get("message.reasoning_content")
379 for _, reasoningText := range collectOpenAIReasoningTexts(reasoningNode) {
482 if reasoningText == "" {380 if reasoningText == "" {
483 continue381 continue
484 }382 }
485 contentBlocks = append(contentBlocks, map[string]interface{}{383 block := `{"type":"thinking","thinking":""}`
486 "type": "thinking",384 block, _ = sjson.Set(block, "thinking", reasoningText)
487 "thinking": reasoningText,385 out, _ = sjson.SetRaw(out, "content.-1", block)
488 })
489 }386 }
490 387
491 // Handle text content388 // Handle text content
492 if content := choice.Get("message.content"); content.Exists() && content.String() != "" {389 if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
493 textBlock := map[string]interface{}{390 block := `{"type":"text","text":""}`
494 "type": "text",391 block, _ = sjson.Set(block, "text", content.String())
495 "text": content.String(),392 out, _ = sjson.SetRaw(out, "content.-1", block)
496 }
497 contentBlocks = append(contentBlocks, textBlock)
498 }393 }
499 394
500 // Handle tool calls395 // Handle tool calls
501 if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {396 if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
502 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {397 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
503 toolUseBlock := map[string]interface{}{398 toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
504 "type": "tool_use",399 toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
505 "id": toolCall.Get("id").String(),400 toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
506 "name": toolCall.Get("function.name").String(),401
507 }402 argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
508 403 if argsStr != "" && gjson.Valid(argsStr) {
509 // Parse arguments404 argsJSON := gjson.Parse(argsStr)
510 argsStr := toolCall.Get("function.arguments").String()405 if argsJSON.IsObject() {
511 argsStr = util.FixJSON(argsStr)406 toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
512 if argsStr != "" {
513 var args interface{}
514 if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
515 toolUseBlock["input"] = args
516 } else {407 } else {
517 toolUseBlock["input"] = map[string]interface{}{}408 toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
518 }409 }
519 } else {410 } else {
520 toolUseBlock["input"] = map[string]interface{}{}411 toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
521 }412 }
522 413
523 contentBlocks = append(contentBlocks, toolUseBlock)414 out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
524 return true415 return true
525 })416 })
526 }417 }
527 418
528 // Set stop reason419 // Set stop reason
529 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {420 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()))
531 }422 }
532 }423 }
533 424
534 response["content"] = contentBlocks
535
536 // Set usage information425 // Set usage information
537 if usage := root.Get("usage"); usage.Exists() {426 if usage := root.Get("usage"); usage.Exists() {
538 response["usage"] = map[string]interface{}{427 out, _ = sjson.Set(out, "usage.input_tokens", usage.Get("prompt_tokens").Int())
539 "input_tokens": usage.Get("prompt_tokens").Int(),428 out, _ = sjson.Set(out, "usage.output_tokens", usage.Get("completion_tokens").Int())
540 "output_tokens": usage.Get("completion_tokens").Int(),429 reasoningTokens := int64(0)
541 "reasoning_tokens": func() int64 {430 if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {
542 if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() {431 reasoningTokens = v.Int()
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,
552 }432 }
433 out, _ = sjson.Set(out, "usage.reasoning_tokens", reasoningTokens)
553 }434 }
554 435
555 responseJSON, _ := json.Marshal(response)436 return []string{out}
556 return []string{string(responseJSON)}
557}437}
558 438
559// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents439// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
@@ -620,12 +500,9 @@ func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, res · 60 unmodified lines
620 if !param.ThinkingContentBlockStarted {500 if !param.ThinkingContentBlockStarted {
621 return501 return
622 }502 }
623 contentBlockStop := map[string]interface{}{503 contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
624 "type": "content_block_stop",504 contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
625 "index": param.ThinkingContentBlockIndex,505 *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
626 }
627 contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
628 *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
629 param.ThinkingContentBlockStarted = false506 param.ThinkingContentBlockStarted = false
630 param.ThinkingContentBlockIndex = -1507 param.ThinkingContentBlockIndex = -1
631}508}
@@ -642,12 +519,9 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results · 10 unmodified lines
642 if !param.TextContentBlockStarted {519 if !param.TextContentBlockStarted {
643 return520 return
644 }521 }
645 contentBlockStop := map[string]interface{}{522 contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
646 "type": "content_block_stop",523 contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex)
647 "index": param.TextContentBlockIndex,524 *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
648 }
649 contentBlockStopJSON, _ := json.Marshal(contentBlockStop)
650 *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n")
651 param.TextContentBlockStarted = false525 param.TextContentBlockStarted = false
652 param.TextContentBlockIndex = -1526 param.TextContentBlockIndex = -1
653}527}
@@ -667,29 +541,19 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 13 unmodified lines
667 _ = requestRawJSON541 _ = requestRawJSON
668 542
669 root := gjson.ParseBytes(rawJSON)543 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())
670 547
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)
686 hasToolCall := false548 hasToolCall := false
549 stopReasonSet := false
687 550
688 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {551 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
689 choice := choices.Array()[0]552 choice := choices.Array()[0]
690 553
691 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {554 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
693 }557 }
694 558
695 if message := choice.Get("message"); message.Exists() {559 if message := choice.Get("message"); message.Exists() {
@@ -702,10 +566,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 6 unmodified lines
702 if textBuilder.Len() == 0 {566 if textBuilder.Len() == 0 {
703 return567 return
704 }568 }
705 contentBlocks = append(contentBlocks, map[string]interface{}{569 block := `{"type":"text","text":""}`
706 "type": "text",570 block, _ = sjson.Set(block, "text", textBuilder.String())
707 "text": textBuilder.String(),571 out, _ = sjson.SetRaw(out, "content.-1", block)
708 })
709 textBuilder.Reset()572 textBuilder.Reset()
710 }573 }
711 574
@@ -713,16 +576,14 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 1 unmodified line
713 if thinkingBuilder.Len() == 0 {576 if thinkingBuilder.Len() == 0 {
714 return577 return
715 }578 }
716 contentBlocks = append(contentBlocks, map[string]interface{}{579 block := `{"type":"thinking","thinking":""}`
717 "type": "thinking",580 block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
718 "thinking": thinkingBuilder.String(),581 out, _ = sjson.SetRaw(out, "content.-1", block)
719 })
720 thinkingBuilder.Reset()582 thinkingBuilder.Reset()
721 }583 }
722 584
723 for _, item := range contentResult.Array() {585 for _, item := range contentResult.Array() {
724 typeStr := item.Get("type").String()586 switch item.Get("type").String() {
725 switch typeStr {
726 case "text":587 case "text":
727 flushThinking()588 flushThinking()
728 textBuilder.WriteString(item.Get("text").String())589 textBuilder.WriteString(item.Get("text").String())
@@ -733,25 +594,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 4 unmodified lines
733 if toolCalls.IsArray() {594 if toolCalls.IsArray() {
734 toolCalls.ForEach(func(_, tc gjson.Result) bool {595 toolCalls.ForEach(func(_, tc gjson.Result) bool {
735 hasToolCall = true596 hasToolCall = true
736 toolUse := map[string]interface{}{597 toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
737 "type": "tool_use",598 toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String())
738 "id": tc.Get("id").String(),599 toolUse, _ = sjson.Set(toolUse, "name", tc.Get("function.name").String())
739 "name": tc.Get("function.name").String(),
740 }
741 600
742 argsStr := util.FixJSON(tc.Get("function.arguments").String())601 argsStr := util.FixJSON(tc.Get("function.arguments").String())
743 if argsStr != "" {602 if argsStr != "" && gjson.Valid(argsStr) {
744 var parsed interface{}603 argsJSON := gjson.Parse(argsStr)
745 if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil {604 if argsJSON.IsObject() {
746 toolUse["input"] = parsed605 toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
747 } else {606 } else {
748 toolUse["input"] = map[string]interface{}{}607 toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
749 }608 }
750 } else {609 } else {
751 toolUse["input"] = map[string]interface{}{}610 toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
752 }611 }
753 612
754 contentBlocks = append(contentBlocks, toolUse)613 out, _ = sjson.SetRaw(out, "content.-1", toolUse)
755 return true614 return true
756 })615 })
757 }616 }
@@ -771,10 +630,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 13 unmodified lines
771 } else if contentResult.Type == gjson.String {630 } else if contentResult.Type == gjson.String {
772 textContent := contentResult.String()631 textContent := contentResult.String()
773 if textContent != "" {632 if textContent != "" {
774 contentBlocks = append(contentBlocks, map[string]interface{}{633 block := `{"type":"text","text":""}`
775 "type": "text",634 block, _ = sjson.Set(block, "text", textContent)
776 "text": textContent,635 out, _ = sjson.SetRaw(out, "content.-1", block)
777 })
778 }636 }
779 }637 }
780 }638 }
@@ -784,81 +642,52 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 3 unmodified lines
784 if reasoningText == "" {642 if reasoningText == "" {
785 continue643 continue
786 }644 }
787 contentBlocks = append(contentBlocks, map[string]interface{}{645 block := `{"type":"thinking","thinking":""}`
788 "type": "thinking",646 block, _ = sjson.Set(block, "thinking", reasoningText)
789 "thinking": reasoningText,647 out, _ = sjson.SetRaw(out, "content.-1", block)
790 })
791 }648 }
792 }649 }
793 650
794 if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {651 if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
795 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {652 toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
796 hasToolCall = true653 hasToolCall = true
797 toolUseBlock := map[string]interface{}{654 toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
798 "type": "tool_use",655 toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String())
799 "id": toolCall.Get("id").String(),656 toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
800 "name": toolCall.Get("function.name").String(),657
801 }658 argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
802 659 if argsStr != "" && gjson.Valid(argsStr) {
803 argsStr := toolCall.Get("function.arguments").String()660 argsJSON := gjson.Parse(argsStr)
804 argsStr = util.FixJSON(argsStr)661 if argsJSON.IsObject() {
805 if argsStr != "" {662 toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
806 var args interface{}
807 if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
808 toolUseBlock["input"] = args
809 } else {663 } else {
810 toolUseBlock["input"] = map[string]interface{}{}664 toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
811 }665 }
812 } else {666 } else {
813 toolUseBlock["input"] = map[string]interface{}{}667 toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
814 }668 }
815 669
816 contentBlocks = append(contentBlocks, toolUseBlock)670 out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock)
817 return true671 return true
818 })672 })
819 }673 }
820 }674 }
821 }675 }
822 676
823 response["content"] = contentBlocks
824
825 if respUsage := root.Get("usage"); respUsage.Exists() {677 if respUsage := root.Get("usage"); respUsage.Exists() {
826 usageJSON := `{}`678 out, _ = sjson.Set(out, "usage.input_tokens", respUsage.Get("prompt_tokens").Int())
827 usageJSON, _ = sjson.Set(usageJSON, "input_tokens", respUsage.Get("prompt_tokens").Int())679 out, _ = sjson.Set(out, "usage.output_tokens", respUsage.Get("completion_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}`
833 }680 }
834 681
835 if response["stop_reason"] == nil {682 if !stopReasonSet {
836 if hasToolCall {683 if hasToolCall {
837 response["stop_reason"] = "tool_use"684 out, _ = sjson.Set(out, "stop_reason", "tool_use")
838 } else {685 } else {
839 response["stop_reason"] = "end_turn"686 out, _ = sjson.Set(out, "stop_reason", "end_turn")
840 }687 }
841 }688 }
842 689
843 if !hasToolCall {690 return out
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)
862}691}
863 692
864func ClaudeTokenCount(ctx context.Context, count int64) string {693func 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
8import (8import (
9 "bytes"9 "bytes"
10 "crypto/rand"10 "crypto/rand"
11 "encoding/json"
12 "fmt"11 "fmt"
13 "math/big"12 "math/big"
14 "strings"13 "strings"
@@ -94,7 +93,6 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 79 unmodified lines
94 out, _ = sjson.Set(out, "stream", stream)93 out, _ = sjson.Set(out, "stream", stream)
95 94
96 // Process contents (Gemini messages) -> OpenAI messages95 // Process contents (Gemini messages) -> OpenAI messages
97 var openAIMessages []interface{}
98 var toolCallIDs []string // Track tool call IDs for matching with tool results96 var toolCallIDs []string // Track tool call IDs for matching with tool results
99 97
100 // System instruction -> OpenAI system message98 // System instruction -> OpenAI system message
@@ -105,22 +103,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 4 unmodified lines
105 }103 }
106 if systemInstruction.Exists() {104 if systemInstruction.Exists() {
107 parts := systemInstruction.Get("parts")105 parts := systemInstruction.Get("parts")
108 msg := map[string]interface{}{106 msg := `{"role":"system","content":[]}`
109 "role": "system",107 hasContent := false
110 "content": []interface{}{},
111 }
112
113 var aggregatedParts []interface{}
114 108
115 if parts.Exists() && parts.IsArray() {109 if parts.Exists() && parts.IsArray() {
116 parts.ForEach(func(_, part gjson.Result) bool {110 parts.ForEach(func(_, part gjson.Result) bool {
117 // Handle text parts111 // Handle text parts
118 if text := part.Get("text"); text.Exists() {112 if text := part.Get("text"); text.Exists() {
119 formattedText := text.String()113 contentPart := `{"type":"text","text":""}`
120 aggregatedParts = append(aggregatedParts, map[string]interface{}{114 contentPart, _ = sjson.Set(contentPart, "text", text.String())
121 "type": "text",115 msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
122 "text": formattedText,116 hasContent = true
123 })
124 }117 }
125 118
126 // Handle inline data (e.g., images)119 // Handle inline data (e.g., images)
@@ -132,20 +125,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 5 unmodified lines
132 data := inlineData.Get("data").String()125 data := inlineData.Get("data").String()
133 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)126 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
134 127
135 aggregatedParts = append(aggregatedParts, map[string]interface{}{128 contentPart := `{"type":"image_url","image_url":{"url":""}}`
136 "type": "image_url",129 contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
137 "image_url": map[string]interface{}{130 msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
138 "url": imageURL,131 hasContent = true
139 },
140 })
141 }132 }
142 return true133 return true
143 })134 })
144 }135 }
145 136
146 if len(aggregatedParts) > 0 {137 if hasContent {
147 msg["content"] = aggregatedParts138 out, _ = sjson.SetRaw(out, "messages.-1", msg)
148 openAIMessages = append(openAIMessages, msg)
149 }139 }
150 }140 }
151 141
@@ -159,16 +149,15 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 7 unmodified lines
159 role = "assistant"149 role = "assistant"
160 }150 }
161 151
162 // Create OpenAI message152 msg := `{"role":"","content":""}`
163 msg := map[string]interface{}{153 msg, _ = sjson.Set(msg, "role", role)
164 "role": role,
165 "content": "",
166 }
167 154
168 var textBuilder strings.Builder155 var textBuilder strings.Builder
169 var aggregatedParts []interface{}156 contentWrapper := `{"arr":[]}`
157 contentPartsCount := 0
170 onlyTextContent := true158 onlyTextContent := true
171 var toolCalls []interface{}159 toolCallsWrapper := `{"arr":[]}`
160 toolCallsCount := 0
172 161
173 if parts.Exists() && parts.IsArray() {162 if parts.Exists() && parts.IsArray() {
174 parts.ForEach(func(_, part gjson.Result) bool {163 parts.ForEach(func(_, part gjson.Result) bool {
@@ -176,10 +165,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line
176 if text := part.Get("text"); text.Exists() {165 if text := part.Get("text"); text.Exists() {
177 formattedText := text.String()166 formattedText := text.String()
178 textBuilder.WriteString(formattedText)167 textBuilder.WriteString(formattedText)
179 aggregatedParts = append(aggregatedParts, map[string]interface{}{168 contentPart := `{"type":"text","text":""}`
180 "type": "text",169 contentPart, _ = sjson.Set(contentPart, "text", formattedText)
181 "text": formattedText,170 contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
182 })171 contentPartsCount++
183 }172 }
184 173
185 // Handle inline data (e.g., images)174 // Handle inline data (e.g., images)
@@ -193,12 +182,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 7 unmodified lines
193 data := inlineData.Get("data").String()182 data := inlineData.Get("data").String()
194 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)183 imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
195 184
196 aggregatedParts = append(aggregatedParts, map[string]interface{}{185 contentPart := `{"type":"image_url","image_url":{"url":""}}`
197 "type": "image_url",186 contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
198 "image_url": map[string]interface{}{187 contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
199 "url": imageURL,188 contentPartsCount++
200 },
201 })
202 }189 }
203 190
204 // Handle function calls (Gemini) -> tool calls (OpenAI)191 // Handle function calls (Gemini) -> tool calls (OpenAI)
@@ -206,44 +193,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line
206 toolCallID := genToolCallID()193 toolCallID := genToolCallID()
207 toolCallIDs = append(toolCallIDs, toolCallID)194 toolCallIDs = append(toolCallIDs, toolCallID)
208 195
209 toolCall := map[string]interface{}{196 toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
210 "id": toolCallID,197 toolCall, _ = sjson.Set(toolCall, "id", toolCallID)
211 "type": "function",198 toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String())
212 "function": map[string]interface{}{
213 "name": functionCall.Get("name").String(),
214 },
215 }
216 199
217 // Convert args to arguments JSON string200 // Convert args to arguments JSON string
218 if args := functionCall.Get("args"); args.Exists() {201 if args := functionCall.Get("args"); args.Exists() {
219 argsJSON, _ := json.Marshal(args.Value())202 toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw)
220 toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON)
221 } else {203 } else {
222 toolCall["function"].(map[string]interface{})["arguments"] = "{}"204 toolCall, _ = sjson.Set(toolCall, "function.arguments", "{}")
223 }205 }
224 206
225 toolCalls = append(toolCalls, toolCall)207 toolCallsWrapper, _ = sjson.SetRaw(toolCallsWrapper, "arr.-1", toolCall)
208 toolCallsCount++
226 }209 }
227 210
228 // Handle function responses (Gemini) -> tool role messages (OpenAI)211 // Handle function responses (Gemini) -> tool role messages (OpenAI)
229 if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {212 if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
230 // Create tool message for function response213 // Create tool message for function response
231 toolMsg := map[string]interface{}{214 toolMsg := `{"role":"tool","tool_call_id":"","content":""}`
232 "role": "tool",
233 "tool_call_id": "", // Will be set based on context
234 "content": "",
235 }
236 215
237 // Convert response.content to JSON string216 // Convert response.content to JSON string
238 if response := functionResponse.Get("response"); response.Exists() {217 if response := functionResponse.Get("response"); response.Exists() {
239 if content = response.Get("content"); content.Exists() {218 if contentField := response.Get("content"); contentField.Exists() {
240 // Use the content field from the response219 toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw)
241 contentJSON, _ := json.Marshal(content.Value())
242 toolMsg["content"] = string(contentJSON)
243 } else {220 } else {
244 // Fallback to entire response221 toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw)
245 responseJSON, _ := json.Marshal(response.Value())
246 toolMsg["content"] = string(responseJSON)
247 }222 }
248 }223 }
249 224
@@ -252,13 +227,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 2 unmodified lines
252 if len(toolCallIDs) > 0 {227 if len(toolCallIDs) > 0 {
253 // Use the last tool call ID (simple matching by function name)228 // Use the last tool call ID (simple matching by function name)
254 // In a real implementation, you might want more sophisticated matching229 // 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])
256 } else {231 } else {
257 // Generate a tool call ID if none available232 // Generate a tool call ID if none available
258 toolMsg["tool_call_id"] = genToolCallID()233 toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", genToolCallID())
259 }234 }
260 235
261 openAIMessages = append(openAIMessages, toolMsg)236 out, _ = sjson.SetRaw(out, "messages.-1", toolMsg)
262 }237 }
263 238
264 return true239 return true
@@ -266,170 +241,46 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line
266 }241 }
267 242
268 // Set content243 // Set content
269 if len(aggregatedParts) > 0 {244 if contentPartsCount > 0 {
270 if onlyTextContent {245 if onlyTextContent {
271 msg["content"] = textBuilder.String()246 msg, _ = sjson.Set(msg, "content", textBuilder.String())
272 } else {247 } else {
273 msg["content"] = aggregatedParts248 msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw)
274 }249 }
275 }250 }
276 251
277 // Set tool calls if any252 // Set tool calls if any
278 if len(toolCalls) > 0 {253 if toolCallsCount > 0 {
279 msg["tool_calls"] = toolCalls254 msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw)
280 }255 }
281 256
282 openAIMessages = append(openAIMessages, msg)257 out, _ = sjson.SetRaw(out, "messages.-1", 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 // }
391 return true258 return true
392 })259 })
393 }260 }
394 261
395 // Set messages
396 if len(openAIMessages) > 0 {
397 messagesJSON, _ := json.Marshal(openAIMessages)
398 out, _ = sjson.SetRaw(out, "messages", string(messagesJSON))
399 }
400
401 // Tools mapping: Gemini tools -> OpenAI tools262 // Tools mapping: Gemini tools -> OpenAI tools
402 if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {263 if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
403 var openAITools []interface{}
404 tools.ForEach(func(_, tool gjson.Result) bool {264 tools.ForEach(func(_, tool gjson.Result) bool {
405 if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {265 if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
406 functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {266 functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
407 openAITool := map[string]interface{}{267 openAITool := `{"type":"function","function":{"name":"","description":""}}`
408 "type": "function",268 openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String())
409 "function": map[string]interface{}{269 openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String())
410 "name": funcDecl.Get("name").String(),
411 "description": funcDecl.Get("description").String(),
412 },
413 }
414 270
415 // Convert parameters schema271 // Convert parameters schema
416 if parameters := funcDecl.Get("parameters"); parameters.Exists() {272 if parameters := funcDecl.Get("parameters"); parameters.Exists() {
417 openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()273 openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
418 } else if parameters = funcDecl.Get("parametersJsonSchema"); parameters.Exists() {274 } else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() {
419 openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value()275 openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw)
420 }276 }
421 277
422 openAITools = append(openAITools, openAITool)278 out, _ = sjson.SetRaw(out, "tools.-1", openAITool)
423 return true279 return true
424 })280 })
425 }281 }
426 return true282 return true
427 })283 })
428
429 if len(openAITools) > 0 {
430 toolsJSON, _ := json.Marshal(openAITools)
431 out, _ = sjson.SetRaw(out, "tools", string(toolsJSON))
432 }
433 }284 }
434 285
435 // Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it)286 // 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
8import (8import (
9 "bytes"9 "bytes"
10 "context"10 "context"
11 "encoding/json"
12 "fmt"11 "fmt"
13 "strconv"12 "strconv"
14 "strings"13 "strings"
@@ -84,15 +83,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 69 unmodified lines
84 template, _ = sjson.Set(template, "model", model.String())83 template, _ = sjson.Set(template, "model", model.String())
85 }84 }
86 85
87 usageObj := map[string]interface{}{86 template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
88 "promptTokenCount": usage.Get("prompt_tokens").Int(),87 template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
89 "candidatesTokenCount": usage.Get("completion_tokens").Int(),88 template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
90 "totalTokenCount": usage.Get("total_tokens").Int(),
91 }
92 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {89 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
93 usageObj["thoughtsTokenCount"] = reasoningTokens90 template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
94 }91 }
95 template, _ = sjson.Set(template, "usageMetadata", usageObj)
96 return []string{template}92 return []string{template}
97 }93 }
98 return []string{}94 return []string{}
@@ -133,13 +129,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 34 unmodified lines
133 continue129 continue
134 }130 }
135 reasoningTemplate := baseTemplate131 reasoningTemplate := baseTemplate
136 parts := []interface{}{132 reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
137 map[string]interface{}{133 reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
138 "thought": true,
139 "text": reasoningText,
140 },
141 }
142 reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts", parts)
143 chunkOutputs = append(chunkOutputs, reasoningTemplate)134 chunkOutputs = append(chunkOutputs, reasoningTemplate)
144 }135 }
145 }136 }
@@ -150,13 +141,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 4 unmodified lines
150 (*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)141 (*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
151 142
152 // Create text part for this delta143 // Create text part for this delta
153 parts := []interface{}{
154 map[string]interface{}{
155 "text": contentText,
156 },
157 }
158 contentTemplate := baseTemplate144 contentTemplate := baseTemplate
159 contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts", parts)145 contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
160 chunkOutputs = append(chunkOutputs, contentTemplate)146 chunkOutputs = append(chunkOutputs, contentTemplate)
161 }147 }
162 148
@@ -225,24 +211,13 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 62 unmodified lines
225 211
226 // If we have accumulated tool calls, output them now212 // If we have accumulated tool calls, output them now
227 if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {213 if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
228 var parts []interface{}214 partIndex := 0
229 for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {215 for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
230 argsStr := accumulator.Arguments.String()216 namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
231 var argsMap map[string]interface{}217 argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
232 218 template, _ = sjson.Set(template, namePath, accumulator.Name)
233 argsMap = parseArgsToMap(argsStr)219 template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
234 220 partIndex++
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)
246 }221 }
247 222
248 // Clear accumulators223 // Clear accumulators
@@ -255,15 +230,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 6 unmodified lines
255 230
256 // Handle usage information231 // Handle usage information
257 if usage := root.Get("usage"); usage.Exists() {232 if usage := root.Get("usage"); usage.Exists() {
258 usageObj := map[string]interface{}{233 template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
259 "promptTokenCount": usage.Get("prompt_tokens").Int(),234 template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
260 "candidatesTokenCount": usage.Get("completion_tokens").Int(),235 template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
261 "totalTokenCount": usage.Get("total_tokens").Int(),
262 }
263 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {236 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
264 usageObj["thoughtsTokenCount"] = reasoningTokens237 template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
265 }238 }
266 template, _ = sjson.Set(template, "usageMetadata", usageObj)
267 results = append(results, template)239 results = append(results, template)
268 return true240 return true
269 }241 }
@@ -291,46 +263,54 @@ func mapOpenAIFinishReasonToGemini(openAIReason string) string { · 21 unmodified lines
291 }263 }
292}264}
293 265
294// parseArgsToMap safely parses a JSON string of function arguments into a map.266// parseArgsToObjectRaw safely parses a JSON string of function arguments into an object JSON string.
295// It returns an empty map if the input is empty or cannot be parsed as a JSON object.267// It returns "{}" if the input is empty or cannot be parsed as a JSON object.
296func parseArgsToMap(argsStr string) map[string]interface{} {268func parseArgsToObjectRaw(argsStr string) string {
297 trimmed := strings.TrimSpace(argsStr)269 trimmed := strings.TrimSpace(argsStr)
298 if trimmed == "" || trimmed == "{}" {270 if trimmed == "" || trimmed == "{}" {
299 return map[string]interface{}{}271 return "{}"
300 }272 }
301 273
302 // First try strict JSON274 // First try strict JSON
303 var out map[string]interface{}275 if gjson.Valid(trimmed) {
304 if errUnmarshal := json.Unmarshal([]byte(trimmed), &out); errUnmarshal == nil {276 strict := gjson.Parse(trimmed)
305 return out277 if strict.IsObject() {
278 return strict.Raw
279 }
306 }280 }
307 281
308 // Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)282 // Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius)
309 tolerant := tolerantParseJSONMap(trimmed)283 tolerant := tolerantParseJSONObjectRaw(trimmed)
310 if len(tolerant) > 0 {284 if tolerant != "{}" {
311 return tolerant285 return tolerant
312 }286 }
313 287
314 // Fallback: return empty object when parsing fails288 // Fallback: return empty object when parsing fails
315 return map[string]interface{}{}289 return "{}"
316}290}
317 291
318// tolerantParseJSONMap attempts to parse a JSON-like object string into a map, tolerating292func 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
319// bareword values (unquoted strings) commonly seen during streamed tool calls.299// bareword values (unquoted strings) commonly seen during streamed tool calls.
320// Example input: {"location": 北京, "unit": celsius}300// Example input: {"location": 北京, "unit": celsius}
321func tolerantParseJSONMap(s string) map[string]interface{} {301func tolerantParseJSONObjectRaw(s string) string {
322 // Ensure we operate within the outermost braces if present302 // Ensure we operate within the outermost braces if present
323 start := strings.Index(s, "{")303 start := strings.Index(s, "{")
324 end := strings.LastIndex(s, "}")304 end := strings.LastIndex(s, "}")
325 if start == -1 || end == -1 || start >= end {305 if start == -1 || end == -1 || start >= end {
326 return map[string]interface{}{}306 return "{}"
327 }307 }
328 content := s[start+1 : end]308 content := s[start+1 : end]
329 309
330 runes := []rune(content)310 runes := []rune(content)
331 n := len(runes)311 n := len(runes)
332 i := 0312 i := 0
333 result := make(map[string]interface{})313 result := "{}"
334 314
335 for i < n {315 for i < n {
336 // Skip whitespace and commas316 // Skip whitespace and commas
@@ -356,6 +336,7 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 19 unmodified lines
356 break336 break
357 }337 }
358 keyName := jsonStringTokenToRawString(keyToken)338 keyName := jsonStringTokenToRawString(keyToken)
339 sjsonKey := escapeSjsonPathKey(keyName)
359 i = nextIdx340 i = nextIdx
360 341
361 // Skip whitespace342 // Skip whitespace
@@ -375,17 +356,16 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 13 unmodified lines
375 }356 }
376 357
377 // Parse value (string, number, object/array, bareword)358 // Parse value (string, number, object/array, bareword)
378 var value interface{}
379 switch runes[i] {359 switch runes[i] {
380 case '"':360 case '"':
381 // JSON string361 // JSON string
382 valToken, ni := parseJSONStringRunes(runes, i)362 valToken, ni := parseJSONStringRunes(runes, i)
383 if ni == -1 {363 if ni == -1 {
384 // Malformed; treat as empty string364 // Malformed; treat as empty string
385 value = ""365 result, _ = sjson.Set(result, sjsonKey, "")
386 i = n366 i = n
387 } else {367 } else {
388 value = jsonStringTokenToRawString(valToken)368 result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
389 i = ni369 i = ni
390 }370 }
391 case '{', '[':371 case '{', '[':
@@ -394,11 +374,10 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 2 unmodified lines
394 if ni == -1 {374 if ni == -1 {
395 i = n375 i = n
396 } else {376 } else {
397 var anyVal interface{}377 if gjson.Valid(seg) {
398 if errUnmarshal := json.Unmarshal([]byte(seg), &anyVal); errUnmarshal == nil {378 result, _ = sjson.SetRaw(result, sjsonKey, seg)
399 value = anyVal
400 } else {379 } else {
401 value = seg380 result, _ = sjson.Set(result, sjsonKey, seg)
402 }381 }
403 i = ni382 i = ni
404 }383 }
@@ -411,21 +390,19 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 6 unmodified lines
411 token := strings.TrimSpace(string(runes[i:j]))390 token := strings.TrimSpace(string(runes[i:j]))
412 // Interpret common JSON atoms and numbers; otherwise treat as string391 // Interpret common JSON atoms and numbers; otherwise treat as string
413 if token == "true" {392 if token == "true" {
414 value = true393 result, _ = sjson.Set(result, sjsonKey, true)
415 } else if token == "false" {394 } else if token == "false" {
416 value = false395 result, _ = sjson.Set(result, sjsonKey, false)
417 } else if token == "null" {396 } else if token == "null" {
418 value = nil397 result, _ = sjson.Set(result, sjsonKey, nil)
419 } else if numVal, ok := tryParseNumber(token); ok {398 } else if numVal, ok := tryParseNumber(token); ok {
420 value = numVal399 result, _ = sjson.Set(result, sjsonKey, numVal)
421 } else {400 } else {
422 value = token401 result, _ = sjson.Set(result, sjsonKey, token)
423 }402 }
424 i = j403 i = j
425 }404 }
426 405
427 result[keyName] = value
428
429 // Skip trailing whitespace and optional comma before next pair406 // Skip trailing whitespace and optional comma before next pair
430 for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {407 for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') {
431 i++408 i++
@@ -463,9 +440,9 @@ func parseJSONStringRunes(runes []rune, start int) (string, int) { · 31 unmodified lines
463 440
464// jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.441// jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value.
465func jsonStringTokenToRawString(token string) string {442func jsonStringTokenToRawString(token string) string {
466 var s string443 r := gjson.Parse(token)
467 if errUnmarshal := json.Unmarshal([]byte(token), &s); errUnmarshal == nil {444 if r.Type == gjson.String {
468 return s445 return r.String()
469 }446 }
470 // Fallback: strip surrounding quotes if present447 // Fallback: strip surrounding quotes if present
471 if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {448 if len(token) >= 2 && token[0] == '"' && token[len(token)-1] == '"' {
@@ -579,7 +556,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 107 unmodified lines
579 }556 }
580 }557 }
581 558
582 var parts []interface{}559 partIndex := 0
583 560
584 // Handle reasoning content before visible text561 // Handle reasoning content before visible text
585 if reasoning := message.Get("reasoning_content"); reasoning.Exists() {562 if reasoning := message.Get("reasoning_content"); reasoning.Exists() {
@@ -587,18 +564,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 1 unmodified line
587 if reasoningText == "" {564 if reasoningText == "" {
588 continue565 continue
589 }566 }
590 parts = append(parts, map[string]interface{}{567 out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
591 "thought": true,568 out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText)
592 "text": reasoningText,569 partIndex++
593 })
594 }570 }
595 }571 }
596 572
597 // Handle content first573 // Handle content first
598 if content := message.Get("content"); content.Exists() && content.String() != "" {574 if content := message.Get("content"); content.Exists() && content.String() != "" {
599 parts = append(parts, map[string]interface{}{575 out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String())
600 "text": content.String(),576 partIndex++
601 })
602 }577 }
603 578
604 // Handle tool calls579 // Handle tool calls
@@ -609,27 +584,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 4 unmodified lines
609 functionName := function.Get("name").String()584 functionName := function.Get("name").String()
610 functionArgs := function.Get("arguments").String()585 functionArgs := function.Get("arguments").String()
611 586
612 // Parse arguments587 namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
613 var argsMap map[string]interface{}588 argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
614 argsMap = parseArgsToMap(functionArgs)589 out, _ = sjson.Set(out, namePath, functionName)
615 590 out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
616 functionCallPart := map[string]interface{}{591 partIndex++
617 "functionCall": map[string]interface{}{
618 "name": functionName,
619 "args": argsMap,
620 },
621 }
622 parts = append(parts, functionCallPart)
623 }592 }
624 return true593 return true
625 })594 })
626 }595 }
627 596
628 // Set parts
629 if len(parts) > 0 {
630 out, _ = sjson.Set(out, "candidates.0.content.parts", parts)
631 }
632
633 // Handle finish reason597 // Handle finish reason
634 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {598 if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
635 geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())599 geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
@@ -645,15 +609,12 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 9 unmodified lines
645 609
646 // Handle usage information610 // Handle usage information
647 if usage := root.Get("usage"); usage.Exists() {611 if usage := root.Get("usage"); usage.Exists() {
648 usageObj := map[string]interface{}{612 out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
649 "promptTokenCount": usage.Get("prompt_tokens").Int(),613 out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
650 "candidatesTokenCount": usage.Get("completion_tokens").Int(),614 out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
651 "totalTokenCount": usage.Get("total_tokens").Int(),
652 }
653 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {615 if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
654 usageObj["thoughtsTokenCount"] = reasoningTokens616 out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
655 }617 }
656 out, _ = sjson.Set(out, "usageMetadata", usageObj)
657 }618 }
658 619
659 return out620 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
484 }484 }
485 }485 }
486 // Build response.output using aggregated buffers486 // Build response.output using aggregated buffers
487 var outputs []interface{}487 outputsWrapper := `{"arr":[]}`
488 if st.ReasoningBuf.Len() > 0 {488 if st.ReasoningBuf.Len() > 0 {
489 outputs = append(outputs, map[string]interface{}{489 item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
490 "id": st.ReasoningID,490 item, _ = sjson.Set(item, "id", st.ReasoningID)
491 "type": "reasoning",491 item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
492 "summary": []interface{}{map[string]interface{}{492 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
493 "type": "summary_text",
494 "text": st.ReasoningBuf.String(),
495 }},
496 })
497 }493 }
498 // Append message items in ascending index order494 // Append message items in ascending index order
499 if len(st.MsgItemAdded) > 0 {495 if len(st.MsgItemAdded) > 0 {
@@ -513,18 +509,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 13 unmodified lines
513 if b := st.MsgTextBuf[i]; b != nil {509 if b := st.MsgTextBuf[i]; b != nil {
514 txt = b.String()510 txt = b.String()
515 }511 }
516 outputs = append(outputs, map[string]interface{}{512 item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
517 "id": fmt.Sprintf("msg_%s_%d", st.ResponseID, i),513 item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
518 "type": "message",514 item, _ = sjson.Set(item, "content.0.text", txt)
519 "status": "completed",515 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
520 "content": []interface{}{map[string]interface{}{
521 "type": "output_text",
522 "annotations": []interface{}{},
523 "logprobs": []interface{}{},
524 "text": txt,
525 }},
526 "role": "assistant",
527 })
528 }516 }
529 }517 }
530 if len(st.FuncArgsBuf) > 0 {518 if len(st.FuncArgsBuf) > 0 {
@@ -547,18 +535,16 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 16 unmodified lines
547 }535 }
548 callID := st.FuncCallIDs[i]536 callID := st.FuncCallIDs[i]
549 name := st.FuncNames[i]537 name := st.FuncNames[i]
550 outputs = append(outputs, map[string]interface{}{538 item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
551 "id": fmt.Sprintf("fc_%s", callID),539 item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
552 "type": "function_call",540 item, _ = sjson.Set(item, "arguments", args)
553 "status": "completed",541 item, _ = sjson.Set(item, "call_id", callID)
554 "arguments": args,542 item, _ = sjson.Set(item, "name", name)
555 "call_id": callID,543 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
556 "name": name,
557 })
558 }544 }
559 }545 }
560 if len(outputs) > 0 {546 if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
561 completed, _ = sjson.Set(completed, "response.output", outputs)547 completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
562 }548 }
563 if st.UsageSeen {549 if st.UsageSeen {
564 completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)550 completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
@@ -681,7 +667,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 116 unmodified lines
681 }667 }
682 668
683 // Build output list from choices[...]669 // Build output list from choices[...]
684 var outputs []interface{}670 outputsWrapper := `{"arr":[]}`
685 // Detect and capture reasoning content if present671 // Detect and capture reasoning content if present
686 rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()672 rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
687 includeReasoning := rcText != ""673 includeReasoning := rcText != ""
@@ -693,21 +679,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 5 unmodified lines
693 if strings.HasPrefix(rid, "resp_") {679 if strings.HasPrefix(rid, "resp_") {
694 rid = strings.TrimPrefix(rid, "resp_")680 rid = strings.TrimPrefix(rid, "resp_")
695 }681 }
696 reasoningItem := map[string]interface{}{
697 "id": fmt.Sprintf("rs_%s", rid),
698 "type": "reasoning",
699 "encrypted_content": "",
700 }
701 // Prefer summary_text from reasoning_content; encrypted_content is optional682 // 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))
703 if rcText != "" {685 if rcText != "" {
704 summaries = append(summaries, map[string]interface{}{686 reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text")
705 "type": "summary_text",687 reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText)
706 "text": rcText,
707 })
708 }688 }
709 reasoningItem["summary"] = summaries689 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem)
710 outputs = append(outputs, reasoningItem)
711 }690 }
712 691
713 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {692 if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
@@ -716,18 +695,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 2 unmodified lines
716 if msg.Exists() {695 if msg.Exists() {
717 // Text message part696 // Text message part
718 if c := msg.Get("content"); c.Exists() && c.String() != "" {697 if c := msg.Get("content"); c.Exists() && c.String() != "" {
719 outputs = append(outputs, map[string]interface{}{698 item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
720 "id": fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())),699 item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
721 "type": "message",700 item, _ = sjson.Set(item, "content.0.text", c.String())
722 "status": "completed",701 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
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 })
731 }702 }
732 703
733 // Function/tool calls704 // Function/tool calls
@@ -736,14 +707,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 2 unmodified lines
736 callID := tc.Get("id").String()707 callID := tc.Get("id").String()
737 name := tc.Get("function.name").String()708 name := tc.Get("function.name").String()
738 args := tc.Get("function.arguments").String()709 args := tc.Get("function.arguments").String()
739 outputs = append(outputs, map[string]interface{}{710 item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
740 "id": fmt.Sprintf("fc_%s", callID),711 item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
741 "type": "function_call",712 item, _ = sjson.Set(item, "arguments", args)
742 "status": "completed",713 item, _ = sjson.Set(item, "call_id", callID)
743 "arguments": args,714 item, _ = sjson.Set(item, "name", name)
744 "call_id": callID,715 outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
745 "name": name,
746 })
747 return true716 return true
748 })717 })
749 }718 }
@@ -751,8 +720,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 1 unmodified line
751 return true720 return true
752 })721 })
753 }722 }
754 if len(outputs) > 0 {723 if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
755 resp, _ = sjson.Set(resp, "output", outputs)724 resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw)
756 }725 }
757 726
758 // usage mapping727 // usage mapping