Unified · Split
| @@ -452,7 +452,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or · 451 unmodified lines | ||
| 452 | 452 | toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) |
| 453 | 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 | 456 | toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw) |
| 457 | 457 | } |
| 458 | 458 | |
| @@ -7,7 +7,6 @@ package gemini · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | − "encoding/json" | |
| 11 | 10 | "fmt" |
| 12 | 11 | |
| 13 | 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 | 135 | // FunctionCallGroup represents a group of function calls and their responses |
| 137 | 136 | type FunctionCallGroup struct { |
| 138 | − ModelContent map[string]interface{} | |
| 139 | − FunctionCalls []gjson.Result | |
| 140 | 137 | ResponsesNeeded int |
| 141 | 138 | } |
| 142 | 139 | |
| 143 | −// parseFunctionResponse attempts to unmarshal a function response part. | |
| 144 | −// Falls back to gjson extraction if standard json.Unmarshal fails. | |
| 145 | −func parseFunctionResponse(response gjson.Result) map[string]interface{} { | |
| 146 | − var responseMap map[string]interface{} | |
| 147 | − err := json.Unmarshal([]byte(response.Raw), &responseMap) | |
| 148 | − if err == nil { | |
| 149 | − return responseMap | |
| 140 | +// parseFunctionResponseRaw attempts to normalize a function response part into a JSON object string. | |
| 141 | +// Falls back to a minimal "functionResponse" object when parsing fails. | |
| 142 | +func parseFunctionResponseRaw(response gjson.Result) string { | |
| 143 | + if response.IsObject() && gjson.Valid(response.Raw) { | |
| 144 | + return response.Raw | |
| 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 | 148 | funcResp := response.Get("functionResponse") |
| 154 | 149 | if funcResp.Exists() { |
| 155 | − fr := map[string]interface{}{ | |
| 156 | − "name": funcResp.Get("name").String(), | |
| 157 | − "response": map[string]interface{}{ | |
| 158 | − "result": funcResp.Get("response").String(), | |
| 159 | − }, | |
| 160 | − } | |
| 150 | + fr := `{"functionResponse":{"name":"","response":{"result":""}}}` | |
| 151 | + fr, _ = sjson.Set(fr, "functionResponse.name", funcResp.Get("name").String()) | |
| 152 | + fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String()) | |
| 161 | 153 | if id := funcResp.Get("id").String(); id != "" { |
| 162 | − fr["id"] = id | |
| 154 | + 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{}{ | |
| 168 | − "functionResponse": map[string]interface{}{ | |
| 169 | − "name": "unknown", | |
| 170 | − "response": map[string]interface{}{"result": response.String()}, | |
| 171 | − }, | |
| 172 | − } | |
| 159 | + fr := `{"functionResponse":{"name":"unknown","response":{"result":""}}}` | |
| 160 | + fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String()) | |
| 161 | + return fr | |
| 173 | 162 | } |
| 174 | 163 | |
| 175 | 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 | 187 | // Initialize data structures for processing and grouping |
| 199 | − var newContents []interface{} // Final processed contents array | |
| 188 | + contentsWrapper := `{"contents":[]}` | |
| 200 | 189 | var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses |
| 201 | 190 | 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 | 217 | collectedResponses = collectedResponses[group.ResponsesNeeded:] |
| 229 | 218 | |
| 230 | 219 | // Create merged function response content |
| 231 | − var responseParts []interface{} | |
| 220 | + functionResponseContent := `{"parts":[],"role":"function"}` | |
| 232 | 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 { | |
| 237 | − functionResponseContent := map[string]interface{}{ | |
| 238 | − "parts": responseParts, | |
| 239 | − "role": "function", | |
| 240 | − } | |
| 241 | − newContents = append(newContents, functionResponseContent) | |
| 228 | + if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { | |
| 229 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) | |
| 242 | 230 | } |
| 243 | 231 | |
| 244 | 232 | // Remove this group as it's been satisfied |
| @@ -252,50 +240,42 @@ func fixCLIToolResponse(input string) (string, error) { · 7 unmodified lines | ||
| 252 | 240 | |
| 253 | 241 | // If this is a model with function calls, create a new group |
| 254 | 242 | if role == "model" { |
| 255 | − var functionCallsInThisModel []gjson.Result | |
| 243 | + functionCallsCount := 0 | |
| 256 | 244 | parts.ForEach(func(_, part gjson.Result) bool { |
| 257 | 245 | if part.Get("functionCall").Exists() { |
| 258 | − functionCallsInThisModel = append(functionCallsInThisModel, part) | |
| 246 | + functionCallsCount++ | |
| 259 | 247 | } |
| 260 | 248 | return true |
| 261 | 249 | }) |
| 262 | 250 | |
| 263 | − if len(functionCallsInThisModel) > 0 { | |
| 251 | + if functionCallsCount > 0 { | |
| 264 | 252 | // Add the model content |
| 265 | − var contentMap map[string]interface{} | |
| 266 | − errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap) | |
| 267 | − if errUnmarshal != nil { | |
| 268 | − log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal) | |
| 253 | + if !value.IsObject() { | |
| 254 | + log.Warnf("failed to parse model content") | |
| 269 | 255 | return true |
| 270 | 256 | } |
| 271 | − newContents = append(newContents, contentMap) | |
| 257 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) | |
| 272 | 258 | |
| 273 | 259 | // Create a new group for tracking responses |
| 274 | 260 | group := &FunctionCallGroup{ |
| 275 | − ModelContent: contentMap, | |
| 276 | − FunctionCalls: functionCallsInThisModel, | |
| 277 | − ResponsesNeeded: len(functionCallsInThisModel), | |
| 261 | + ResponsesNeeded: functionCallsCount, | |
| 278 | 262 | } |
| 279 | 263 | pendingGroups = append(pendingGroups, group) |
| 280 | 264 | } else { |
| 281 | 265 | // Regular model content without function calls |
| 282 | − var contentMap map[string]interface{} | |
| 283 | − errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap) | |
| 284 | − if errUnmarshal != nil { | |
| 285 | − log.Warnf("failed to unmarshal content: %v\n", errUnmarshal) | |
| 266 | + if !value.IsObject() { | |
| 267 | + log.Warnf("failed to parse content") | |
| 286 | 268 | return true |
| 287 | 269 | } |
| 288 | − newContents = append(newContents, contentMap) | |
| 270 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) | |
| 289 | 271 | } |
| 290 | 272 | } else { |
| 291 | 273 | // Non-model content (user, etc.) |
| 292 | − var contentMap map[string]interface{} | |
| 293 | − errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap) | |
| 294 | − if errUnmarshal != nil { | |
| 295 | − log.Warnf("failed to unmarshal content: %v\n", errUnmarshal) | |
| 274 | + if !value.IsObject() { | |
| 275 | + log.Warnf("failed to parse content") | |
| 296 | 276 | return true |
| 297 | 277 | } |
| 298 | − newContents = append(newContents, contentMap) | |
| 278 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) | |
| 299 | 279 | } |
| 300 | 280 | |
| 301 | 281 | return true |
| @@ -307,25 +287,23 @@ func fixCLIToolResponse(input string) (string, error) { · 5 unmodified lines | ||
| 307 | 287 | groupResponses := collectedResponses[:group.ResponsesNeeded] |
| 308 | 288 | collectedResponses = collectedResponses[group.ResponsesNeeded:] |
| 309 | 289 | |
| 310 | − var responseParts []interface{} | |
| 290 | + functionResponseContent := `{"parts":[],"role":"function"}` | |
| 311 | 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 { | |
| 316 | − functionResponseContent := map[string]interface{}{ | |
| 317 | − "parts": responseParts, | |
| 318 | − "role": "function", | |
| 319 | − } | |
| 320 | − newContents = append(newContents, functionResponseContent) | |
| 298 | + if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { | |
| 299 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) | |
| 321 | 300 | } |
| 322 | 301 | } |
| 323 | 302 | } |
| 324 | 303 | |
| 325 | 304 | // Update the original JSON with the new contents |
| 326 | 305 | result := input |
| 327 | − newContentsJSON, _ := json.Marshal(newContents) | |
| 328 | − result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON)) | |
| 306 | + result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw) | |
| 329 | 307 | |
| 330 | 308 | return result, nil |
| 331 | 309 | } |
| @@ -319,7 +319,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 318 unmodified lines | ||
| 319 | 319 | log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) |
| 320 | 320 | continue |
| 321 | 321 | } |
| 322 | − fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{}) | |
| 322 | + fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) | |
| 323 | 323 | if errSet != nil { |
| 324 | 324 | log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) |
| 325 | 325 | continue |
| @@ -334,7 +334,7 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ · 8 unmodified lines | ||
| 334 | 334 | log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) |
| 335 | 335 | continue |
| 336 | 336 | } |
| 337 | − fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{}) | |
| 337 | + fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) | |
| 338 | 338 | if errSet != nil { |
| 339 | 339 | log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) |
| 340 | 340 | continue |
| @@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "context" |
| 11 | − "encoding/json" | |
| 12 | 11 | "fmt" |
| 13 | 12 | "strings" |
| 14 | 13 | "sync/atomic" |
| @@ -171,21 +170,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq · 156 unmodified lines | ||
| 171 | 170 | mimeType = "image/png" |
| 172 | 171 | } |
| 173 | 172 | imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) |
| 174 | − imagePayload, err := json.Marshal(map[string]any{ | |
| 175 | − "type": "image_url", | |
| 176 | − "image_url": map[string]string{ | |
| 177 | − "url": imageURL, | |
| 178 | − }, | |
| 179 | − }) | |
| 180 | − if err != nil { | |
| 181 | − continue | |
| 182 | − } | |
| 173 | + imagePayload := `{"image_url":{"url":""},"type":"image_url"}` | |
| 174 | + imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) | |
| 183 | 175 | imagesResult := gjson.Get(template, "choices.0.delta.images") |
| 184 | 176 | if !imagesResult.Exists() || !imagesResult.IsArray() { |
| 185 | 177 | template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`) |
| 186 | 178 | } |
| 187 | 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 | } |
| @@ -194,7 +194,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream · 193 unmodified lines | ||
| 194 | 194 | if name := fc.Get("name"); name.Exists() { |
| 195 | 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 | 198 | toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw) |
| 199 | 199 | } |
| 200 | 200 | msg, _ = sjson.SetRaw(msg, "content.-1", toolUse) |
| @@ -314,11 +314,11 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream · 113 unmodified lines | ||
| 314 | 314 | if mode := funcCalling.Get("mode"); mode.Exists() { |
| 315 | 315 | switch mode.String() { |
| 316 | 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 | 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 | 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 | } |
| @@ -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 | |
| 267 | −func convertArrayToJSON(arr []interface{}) string { | |
| 268 | − result := "[]" | |
| 269 | − for _, item := range arr { | |
| 270 | − switch itemData := item.(type) { | |
| 271 | − case map[string]interface{}: | |
| 272 | − itemJSON := convertMapToJSON(itemData) | |
| 273 | − result, _ = sjson.SetRaw(result, "-1", itemJSON) | |
| 274 | − case string: | |
| 275 | − result, _ = sjson.Set(result, "-1", itemData) | |
| 276 | − case bool: | |
| 277 | − result, _ = sjson.Set(result, "-1", itemData) | |
| 278 | − case float64, int, int64: | |
| 279 | − result, _ = sjson.Set(result, "-1", itemData) | |
| 280 | − default: | |
| 281 | − result, _ = sjson.Set(result, "-1", itemData) | |
| 282 | − } | |
| 283 | − } | |
| 284 | − return result | |
| 285 | −} | |
| 286 | − | |
| 287 | −// convertMapToJSON converts map[string]interface{} to JSON object string | |
| 288 | −func convertMapToJSON(m map[string]interface{}) string { | |
| 289 | − result := "{}" | |
| 290 | − for key, value := range m { | |
| 291 | − switch val := value.(type) { | |
| 292 | − case map[string]interface{}: | |
| 293 | − nestedJSON := convertMapToJSON(val) | |
| 294 | − result, _ = sjson.SetRaw(result, key, nestedJSON) | |
| 295 | − case []interface{}: | |
| 296 | − arrayJSON := convertArrayToJSON(val) | |
| 297 | − result, _ = sjson.SetRaw(result, key, arrayJSON) | |
| 298 | − case string: | |
| 299 | − result, _ = sjson.Set(result, key, val) | |
| 300 | − case bool: | |
| 301 | − result, _ = sjson.Set(result, key, val) | |
| 302 | − case float64, int, int64: | |
| 303 | − result, _ = sjson.Set(result, key, val) | |
| 304 | − default: | |
| 305 | − result, _ = sjson.Set(result, key, val) | |
| 306 | − } | |
| 307 | − } | |
| 308 | − return result | |
| 309 | −} | |
| 310 | − | |
| 311 | 266 | // ConvertClaudeResponseToGeminiNonStream converts a non-streaming Claude Code response to a non-streaming Gemini response. |
| 312 | 267 | // This function processes the complete Claude Code response and transforms it into a single Gemini-compatible |
| 313 | 268 | // 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 | 313 | // Process each streaming event and collect parts |
| 359 | − var allParts []interface{} | |
| 360 | − var finalUsage map[string]interface{} | |
| 314 | + var allParts []string | |
| 315 | + var finalUsageJSON string | |
| 361 | 316 | var responseID string |
| 362 | 317 | var createdAt int64 |
| 363 | 318 | |
| @@ -407,16 +362,14 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 43 unmodified lines | ||
| 407 | 362 | if text := delta.Get("text"); text.Exists() && text.String() != "" { |
| 408 | 363 | partJSON := `{"text":""}` |
| 409 | 364 | partJSON, _ = sjson.Set(partJSON, "text", text.String()) |
| 410 | − part := gjson.Parse(partJSON).Value().(map[string]interface{}) | |
| 411 | − allParts = append(allParts, part) | |
| 365 | + allParts = append(allParts, partJSON) | |
| 412 | 366 | } |
| 413 | 367 | case "thinking_delta": |
| 414 | 368 | // Process reasoning/thinking content |
| 415 | 369 | if text := delta.Get("thinking"); text.Exists() && text.String() != "" { |
| 416 | 370 | partJSON := `{"thought":true,"text":""}` |
| 417 | 371 | partJSON, _ = sjson.Set(partJSON, "text", text.String()) |
| 418 | − part := gjson.Parse(partJSON).Value().(map[string]interface{}) | |
| 419 | − allParts = append(allParts, part) | |
| 372 | + allParts = append(allParts, partJSON) | |
| 420 | 373 | } |
| 421 | 374 | case "input_json_delta": |
| 422 | 375 | // accumulate args partial_json for this index |
| @@ -456,9 +409,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 33 unmodified lines | ||
| 456 | 409 | if argsTrim != "" { |
| 457 | 410 | functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim) |
| 458 | 411 | } |
| 459 | − // Parse back to interface{} for allParts | |
| 460 | − functionCall := gjson.Parse(functionCallJSON).Value().(map[string]interface{}) | |
| 461 | − allParts = append(allParts, functionCall) | |
| 412 | + allParts = append(allParts, functionCallJSON) | |
| 462 | 413 | // cleanup used state for this index |
| 463 | 414 | if newParam.ToolUseArgs != nil { |
| 464 | 415 | delete(newParam.ToolUseArgs, idx) |
| @@ -501,8 +452,7 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 36 unmodified lines | ||
| 501 | 452 | // Set traffic type (required by Gemini API) |
| 502 | 453 | usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT") |
| 503 | 454 | |
| 504 | − // Convert to map[string]interface{} using gjson | |
| 505 | − finalUsage = gjson.Parse(usageJSON).Value().(map[string]interface{}) | |
| 455 | + finalUsageJSON = usageJSON | |
| 506 | 456 | } |
| 507 | 457 | } |
| 508 | 458 | } |
| @@ -520,12 +470,16 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, · 11 unmodified lines | ||
| 520 | 470 | |
| 521 | 471 | // Set the consolidated parts array |
| 522 | 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 | 480 | // Set usage metadata |
| 527 | − if finalUsage != nil { | |
| 528 | − template, _ = sjson.SetRaw(template, "usageMetadata", convertToJSONString(finalUsage)) | |
| 481 | + if finalUsageJSON != "" { | |
| 482 | + template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON) | |
| 529 | 483 | } |
| 530 | 484 | |
| 531 | 485 | return template |
| @@ -539,12 +493,12 @@ func GeminiTokenCount(ctx context.Context, count int64) string { · 7 unmodified lines | ||
| 539 | 493 | // This function processes the parts array to combine adjacent text elements and thinking elements |
| 540 | 494 | // into single consolidated parts, which results in a more readable and efficient response structure. |
| 541 | 495 | // Tool calls and other non-text parts are preserved as separate elements. |
| 542 | −func consolidateParts(parts []interface{}) []interface{} { | |
| 496 | +func consolidateParts(parts []string) []string { | |
| 543 | 497 | if len(parts) == 0 { |
| 544 | 498 | return parts |
| 545 | 499 | } |
| 546 | 500 | |
| 547 | − var consolidated []interface{} | |
| 501 | + var consolidated []string | |
| 548 | 502 | var currentTextPart strings.Builder |
| 549 | 503 | var currentThoughtPart strings.Builder |
| 550 | 504 | var hasText, hasThought bool |
| @@ -554,8 +508,7 @@ func consolidateParts(parts []interface{}) []interface{} { · 3 unmodified lines | ||
| 554 | 508 | if hasText && currentTextPart.Len() > 0 { |
| 555 | 509 | textPartJSON := `{"text":""}` |
| 556 | 510 | textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String()) |
| 557 | − textPart := gjson.Parse(textPartJSON).Value().(map[string]interface{}) | |
| 558 | − consolidated = append(consolidated, textPart) | |
| 511 | + consolidated = append(consolidated, textPartJSON) | |
| 559 | 512 | currentTextPart.Reset() |
| 560 | 513 | hasText = false |
| 561 | 514 | } |
| @@ -566,42 +519,42 @@ func consolidateParts(parts []interface{}) []interface{} { · 4 unmodified lines | ||
| 566 | 519 | if hasThought && currentThoughtPart.Len() > 0 { |
| 567 | 520 | thoughtPartJSON := `{"thought":true,"text":""}` |
| 568 | 521 | thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String()) |
| 569 | − thoughtPart := gjson.Parse(thoughtPartJSON).Value().(map[string]interface{}) | |
| 570 | − consolidated = append(consolidated, thoughtPart) | |
| 522 | + consolidated = append(consolidated, thoughtPartJSON) | |
| 571 | 523 | currentThoughtPart.Reset() |
| 572 | 524 | hasThought = false |
| 573 | 525 | } |
| 574 | 526 | } |
| 575 | 527 | |
| 576 | − for _, part := range parts { | |
| 577 | − partMap, ok := part.(map[string]interface{}) | |
| 578 | − if !ok { | |
| 528 | + for _, partJSON := range parts { | |
| 529 | + part := gjson.Parse(partJSON) | |
| 530 | + if !part.Exists() || !part.IsObject() { | |
| 579 | 531 | // Flush any pending parts and add this non-text part |
| 580 | 532 | flushText() |
| 581 | 533 | flushThought() |
| 582 | − consolidated = append(consolidated, part) | |
| 534 | + consolidated = append(consolidated, partJSON) | |
| 583 | 535 | 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 | 540 | // This is a thinking part - flush any pending text first |
| 588 | 541 | flushText() // Flush any pending text first |
| 589 | 542 | |
| 590 | − if text, hasTextContent := partMap["text"].(string); hasTextContent { | |
| 591 | − currentThoughtPart.WriteString(text) | |
| 543 | + if text := part.Get("text"); text.Exists() && text.Type == gjson.String { | |
| 544 | + currentThoughtPart.WriteString(text.String()) | |
| 592 | 545 | 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 | 548 | // This is a regular text part - flush any pending thought first |
| 596 | 549 | flushThought() // Flush any pending thought first |
| 597 | 550 | |
| 598 | − currentTextPart.WriteString(text) | |
| 551 | + currentTextPart.WriteString(text.String()) | |
| 599 | 552 | hasText = true |
| 600 | 553 | } else { |
| 601 | 554 | // This is some other type of part (like function call) - flush both text and thought |
| 602 | 555 | flushText() |
| 603 | 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 | 565 | 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. | |
| 618 | −func convertToJSONString(v interface{}) string { | |
| 619 | − switch val := v.(type) { | |
| 620 | − case []interface{}: | |
| 621 | − return convertArrayToJSON(val) | |
| 622 | − case map[string]interface{}: | |
| 623 | − return convertMapToJSON(val) | |
| 624 | − default: | |
| 625 | − // For simple types, create a temporary JSON and extract the value | |
| 626 | − temp := `{"temp":null}` | |
| 627 | − temp, _ = sjson.Set(temp, "temp", val) | |
| 628 | − return gjson.Get(temp, "temp").Raw | |
| 629 | − } | |
| 630 | −} | |
| @@ -10,7 +10,6 @@ import ( · 9 unmodified lines | ||
| 10 | 10 | "crypto/rand" |
| 11 | 11 | "crypto/sha256" |
| 12 | 12 | "encoding/hex" |
| 13 | − "encoding/json" | |
| 14 | 13 | "fmt" |
| 15 | 14 | "math/big" |
| 16 | 15 | "strings" |
| @@ -137,9 +136,6 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 120 unmodified lines | ||
| 137 | 136 | out, _ = sjson.Set(out, "stream", stream) |
| 138 | 137 | |
| 139 | 138 | // 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 | 139 | if messages := root.Get("messages"); messages.Exists() && messages.IsArray() { |
| 144 | 140 | messages.ForEach(func(_, message gjson.Result) bool { |
| 145 | 141 | role := message.Get("role").String() |
| @@ -152,33 +148,23 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines | ||
| 152 | 148 | role = "user" |
| 153 | 149 | } |
| 154 | 150 | |
| 155 | − msg := map[string]interface{}{ | |
| 156 | − "role": role, | |
| 157 | − "content": []interface{}{}, | |
| 158 | − } | |
| 151 | + msg := `{"role":"","content":[]}` | |
| 152 | + msg, _ = sjson.Set(msg, "role", role) | |
| 159 | 153 | |
| 160 | 154 | // Handle content based on its type (string or array) |
| 161 | 155 | if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" { |
| 162 | − // Simple text content conversion | |
| 163 | − msg["content"] = []interface{}{ | |
| 164 | − map[string]interface{}{ | |
| 165 | − "type": "text", | |
| 166 | − "text": contentResult.String(), | |
| 167 | − }, | |
| 168 | − } | |
| 156 | + part := `{"type":"text","text":""}` | |
| 157 | + part, _ = sjson.Set(part, "text", contentResult.String()) | |
| 158 | + msg, _ = sjson.SetRaw(msg, "content.-1", part) | |
| 169 | 159 | } else if contentResult.Exists() && contentResult.IsArray() { |
| 170 | − // Array of content parts processing | |
| 171 | − var contentParts []interface{} | |
| 172 | 160 | contentResult.ForEach(func(_, part gjson.Result) bool { |
| 173 | 161 | partType := part.Get("type").String() |
| 174 | 162 | |
| 175 | 163 | switch partType { |
| 176 | 164 | case "text": |
| 177 | − // Text part conversion | |
| 178 | − contentParts = append(contentParts, map[string]interface{}{ | |
| 179 | − "type": "text", | |
| 180 | − "text": part.Get("text").String(), | |
| 181 | − }) | |
| 165 | + textPart := `{"type":"text","text":""}` | |
| 166 | + textPart, _ = sjson.Set(textPart, "text", part.Get("text").String()) | |
| 167 | + msg, _ = sjson.SetRaw(msg, "content.-1", textPart) | |
| 182 | 168 | |
| 183 | 169 | case "image_url": |
| 184 | 170 | // Convert OpenAI image format to Claude Code format |
| @@ -191,132 +177,95 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines | ||
| 191 | 177 | mediaType := strings.TrimPrefix(mediaTypePart, "data:") |
| 192 | 178 | data := parts[1] |
| 193 | 179 | |
| 194 | − contentParts = append(contentParts, map[string]interface{}{ | |
| 195 | − "type": "image", | |
| 196 | − "source": map[string]interface{}{ | |
| 197 | − "type": "base64", | |
| 198 | − "media_type": mediaType, | |
| 199 | − "data": data, | |
| 200 | − }, | |
| 201 | − }) | |
| 180 | + imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}` | |
| 181 | + imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType) | |
| 182 | + imagePart, _ = sjson.Set(imagePart, "source.data", data) | |
| 183 | + msg, _ = sjson.SetRaw(msg, "content.-1", imagePart) | |
| 202 | 184 | } |
| 203 | 185 | } |
| 204 | 186 | } |
| 205 | 187 | 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 | 191 | // Handle tool calls (for assistant messages) |
| 216 | 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 | 193 | toolCalls.ForEach(func(_, toolCall gjson.Result) bool { |
| 225 | 194 | if toolCall.Get("type").String() == "function" { |
| 226 | 195 | toolCallID := toolCall.Get("id").String() |
| 227 | 196 | if toolCallID == "" { |
| 228 | 197 | toolCallID = genToolCallID() |
| 229 | 198 | } |
| 230 | − toolCallIDs = append(toolCallIDs, toolCallID) | |
| 231 | 199 | |
| 232 | 200 | function := toolCall.Get("function") |
| 233 | − toolUse := map[string]interface{}{ | |
| 234 | − "type": "tool_use", | |
| 235 | − "id": toolCallID, | |
| 236 | − "name": function.Get("name").String(), | |
| 237 | − } | |
| 201 | + toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 202 | + toolUse, _ = sjson.Set(toolUse, "id", toolCallID) | |
| 203 | + toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String()) | |
| 238 | 204 | |
| 239 | 205 | // Parse arguments for the tool call |
| 240 | 206 | if args := function.Get("arguments"); args.Exists() { |
| 241 | 207 | argsStr := args.String() |
| 242 | − if argsStr != "" { | |
| 243 | − var argsMap map[string]interface{} | |
| 244 | − if err := json.Unmarshal([]byte(argsStr), &argsMap); err == nil { | |
| 245 | − toolUse["input"] = argsMap | |
| 208 | + if argsStr != "" && gjson.Valid(argsStr) { | |
| 209 | + argsJSON := gjson.Parse(argsStr) | |
| 210 | + if argsJSON.IsObject() { | |
| 211 | + toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw) | |
| 246 | 212 | } else { |
| 247 | − toolUse["input"] = map[string]interface{}{} | |
| 213 | + toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") | |
| 248 | 214 | } |
| 249 | 215 | } else { |
| 250 | − toolUse["input"] = map[string]interface{}{} | |
| 216 | + toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") | |
| 251 | 217 | } |
| 252 | 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 | 224 | 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 | 230 | case "tool": |
| 266 | 231 | // Handle tool result messages conversion |
| 267 | 232 | toolCallID := message.Get("tool_call_id").String() |
| 268 | 233 | content := message.Get("content").String() |
| 269 | 234 | |
| 270 | − // Create tool result message in Claude Code format | |
| 271 | − msg := map[string]interface{}{ | |
| 272 | − "role": "user", | |
| 273 | − "content": []interface{}{ | |
| 274 | − map[string]interface{}{ | |
| 275 | − "type": "tool_result", | |
| 276 | − "tool_use_id": toolCallID, | |
| 277 | − "content": content, | |
| 278 | − }, | |
| 279 | − }, | |
| 280 | − } | |
| 281 | − | |
| 282 | − anthropicMessages = append(anthropicMessages, msg) | |
| 235 | + msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}` | |
| 236 | + msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID) | |
| 237 | + msg, _ = sjson.Set(msg, "content.0.content", content) | |
| 238 | + out, _ = sjson.SetRaw(out, "messages.-1", msg) | |
| 283 | 239 | } |
| 284 | 240 | 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 | 244 | // Tools mapping: OpenAI tools -> Claude Code tools |
| 295 | 245 | if tools := root.Get("tools"); tools.Exists() && tools.IsArray() && len(tools.Array()) > 0 { |
| 296 | − var anthropicTools []interface{} | |
| 246 | + hasAnthropicTools := false | |
| 297 | 247 | tools.ForEach(func(_, tool gjson.Result) bool { |
| 298 | 248 | if tool.Get("type").String() == "function" { |
| 299 | 249 | function := tool.Get("function") |
| 300 | − anthropicTool := map[string]interface{}{ | |
| 301 | − "name": function.Get("name").String(), | |
| 302 | − "description": function.Get("description").String(), | |
| 303 | − } | |
| 250 | + anthropicTool := `{"name":"","description":""}` | |
| 251 | + anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String()) | |
| 252 | + anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String()) | |
| 304 | 253 | |
| 305 | 254 | // Convert parameters schema for the tool |
| 306 | 255 | if parameters := function.Get("parameters"); parameters.Exists() { |
| 307 | − anthropicTool["input_schema"] = parameters.Value() | |
| 308 | − } else if parameters = function.Get("parametersJsonSchema"); parameters.Exists() { | |
| 309 | − anthropicTool["input_schema"] = parameters.Value() | |
| 256 | + anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw) | |
| 257 | + } else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() { | |
| 258 | + anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw) | |
| 310 | 259 | } |
| 311 | 260 | |
| 312 | − anthropicTools = append(anthropicTools, anthropicTool) | |
| 261 | + out, _ = sjson.SetRaw(out, "tools.-1", anthropicTool) | |
| 262 | + hasAnthropicTools = true | |
| 313 | 263 | } |
| 314 | 264 | return true |
| 315 | 265 | }) |
| 316 | 266 | |
| 317 | − if len(anthropicTools) > 0 { | |
| 318 | − toolsJSON, _ := json.Marshal(anthropicTools) | |
| 319 | − out, _ = sjson.SetRaw(out, "tools", string(toolsJSON)) | |
| 267 | + if !hasAnthropicTools { | |
| 268 | + out, _ = sjson.Delete(out, "tools") | |
| 320 | 269 | } |
| 321 | 270 | } |
| 322 | 271 | |
| @@ -329,18 +278,17 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream · 6 unmodified lines | ||
| 329 | 278 | case "none": |
| 330 | 279 | // Don't set tool_choice, Claude Code will not use tools |
| 331 | 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 | 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 | 285 | case gjson.JSON: |
| 337 | 286 | // Specific tool choice mapping |
| 338 | 287 | if toolChoice.Get("type").String() == "function" { |
| 339 | 288 | functionName := toolChoice.Get("function.name").String() |
| 340 | − out, _ = sjson.Set(out, "tool_choice", map[string]interface{}{ | |
| 341 | − "type": "tool", | |
| 342 | − "name": functionName, | |
| 343 | − }) | |
| 289 | + toolChoiceJSON := `{"type":"tool","name":""}` | |
| 290 | + toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName) | |
| 291 | + out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON) | |
| 344 | 292 | } |
| 345 | 293 | default: |
| 346 | 294 | } |
| @@ -8,7 +8,7 @@ package chat_completions · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "context" |
| 11 | − "encoding/json" | |
| 11 | + "fmt" | |
| 12 | 12 | "strings" |
| 13 | 13 | "time" |
| 14 | 14 | |
| @@ -182,18 +182,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 167 unmodified lines | ||
| 182 | 182 | if arguments == "" { |
| 183 | 183 | arguments = "{}" |
| 184 | 184 | } |
| 185 | − | |
| 186 | − toolCall := map[string]interface{}{ | |
| 187 | − "index": index, | |
| 188 | − "id": accumulator.ID, | |
| 189 | − "type": "function", | |
| 190 | − "function": map[string]interface{}{ | |
| 191 | − "name": accumulator.Name, | |
| 192 | − "arguments": arguments, | |
| 193 | − }, | |
| 194 | − } | |
| 195 | − | |
| 196 | − template, _ = sjson.Set(template, "choices.0.delta.tool_calls", []interface{}{toolCall}) | |
| 185 | + template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index) | |
| 186 | + template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID) | |
| 187 | + template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function") | |
| 188 | + template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name) | |
| 189 | + template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.arguments", arguments) | |
| 197 | 190 | |
| 198 | 191 | // Clean up the accumulator for this index |
| 199 | 192 | delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index) |
| @@ -214,12 +207,11 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 14 unmodified lines | ||
| 214 | 207 | |
| 215 | 208 | // Handle usage information for token counts |
| 216 | 209 | if usage := root.Get("usage"); usage.Exists() { |
| 217 | − usageObj := map[string]interface{}{ | |
| 218 | − "prompt_tokens": usage.Get("input_tokens").Int(), | |
| 219 | − "completion_tokens": usage.Get("output_tokens").Int(), | |
| 220 | − "total_tokens": usage.Get("input_tokens").Int() + usage.Get("output_tokens").Int(), | |
| 221 | − } | |
| 222 | − template, _ = sjson.Set(template, "usage", usageObj) | |
| 210 | + inputTokens := usage.Get("input_tokens").Int() | |
| 211 | + outputTokens := usage.Get("output_tokens").Int() | |
| 212 | + template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens) | |
| 213 | + template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens) | |
| 214 | + template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens) | |
| 223 | 215 | } |
| 224 | 216 | return []string{template} |
| 225 | 217 | |
| @@ -234,14 +226,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original · 8 unmodified lines | ||
| 234 | 226 | case "error": |
| 235 | 227 | // Error event - format and return error response |
| 236 | 228 | if errorData := root.Get("error"); errorData.Exists() { |
| 237 | − errorResponse := map[string]interface{}{ | |
| 238 | − "error": map[string]interface{}{ | |
| 239 | − "message": errorData.Get("message").String(), | |
| 240 | − "type": errorData.Get("type").String(), | |
| 241 | − }, | |
| 242 | − } | |
| 243 | − errorJSON, _ := json.Marshal(errorResponse) | |
| 244 | − return []string{string(errorJSON)} | |
| 229 | + errorJSON := `{"error":{"message":"","type":""}}` | |
| 230 | + errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String()) | |
| 231 | + errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String()) | |
| 232 | + return []string{errorJSON} | |
| 245 | 233 | } |
| 246 | 234 | return []string{} |
| 247 | 235 | |
| @@ -302,10 +290,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 54 unmodified lines | ||
| 302 | 290 | var stopReason string |
| 303 | 291 | var contentParts []string |
| 304 | 292 | var reasoningParts []string |
| 305 | − // Use map to track tool calls by index for proper merging | |
| 306 | − toolCallsMap := make(map[int]map[string]interface{}) | |
| 307 | − // Track tool call arguments accumulation | |
| 308 | − toolCallArgsMap := make(map[int]strings.Builder) | |
| 293 | + toolCallsAccumulator := make(map[int]*ToolCallAccumulator) | |
| 309 | 294 | |
| 310 | 295 | for _, chunk := range chunks { |
| 311 | 296 | root := gjson.ParseBytes(chunk) |
| @@ -331,18 +316,12 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 19 unmodified lines | ||
| 331 | 316 | // Start of thinking/reasoning content - skip for now as it's handled in delta |
| 332 | 317 | continue |
| 333 | 318 | } else if blockType == "tool_use" { |
| 334 | − // Initialize tool call tracking for this index | |
| 319 | + // Initialize tool call accumulator for this index | |
| 335 | 320 | index := int(root.Get("index").Int()) |
| 336 | − toolCallsMap[index] = map[string]interface{}{ | |
| 337 | − "id": contentBlock.Get("id").String(), | |
| 338 | − "type": "function", | |
| 339 | − "function": map[string]interface{}{ | |
| 340 | − "name": contentBlock.Get("name").String(), | |
| 341 | − "arguments": "", | |
| 342 | − }, | |
| 321 | + toolCallsAccumulator[index] = &ToolCallAccumulator{ | |
| 322 | + ID: contentBlock.Get("id").String(), | |
| 323 | + Name: contentBlock.Get("name").String(), | |
| 343 | 324 | } |
| 344 | − // Initialize arguments builder for this tool call | |
| 345 | − toolCallArgsMap[index] = strings.Builder{} | |
| 346 | 325 | } |
| 347 | 326 | } |
| 348 | 327 | |
| @@ -365,9 +344,8 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 16 unmodified lines | ||
| 365 | 344 | // Accumulate tool call arguments |
| 366 | 345 | if partialJSON := delta.Get("partial_json"); partialJSON.Exists() { |
| 367 | 346 | index := int(root.Get("index").Int()) |
| 368 | − if builder, exists := toolCallArgsMap[index]; exists { | |
| 369 | − builder.WriteString(partialJSON.String()) | |
| 370 | − toolCallArgsMap[index] = builder | |
| 347 | + if accumulator, exists := toolCallsAccumulator[index]; exists { | |
| 348 | + accumulator.Arguments.WriteString(partialJSON.String()) | |
| 371 | 349 | } |
| 372 | 350 | } |
| 373 | 351 | } |
| @@ -376,14 +354,9 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 2 unmodified lines | ||
| 376 | 354 | case "content_block_stop": |
| 377 | 355 | // Finalize tool call arguments for this index when content block ends |
| 378 | 356 | index := int(root.Get("index").Int()) |
| 379 | − if toolCall, exists := toolCallsMap[index]; exists { | |
| 380 | − if builder, argsExists := toolCallArgsMap[index]; argsExists { | |
| 381 | − // Set the accumulated arguments for the tool call | |
| 382 | − arguments := builder.String() | |
| 383 | − if arguments == "" { | |
| 384 | − arguments = "{}" | |
| 385 | − } | |
| 386 | − toolCall["function"].(map[string]interface{})["arguments"] = arguments | |
| 357 | + if accumulator, exists := toolCallsAccumulator[index]; exists { | |
| 358 | + if accumulator.Arguments.Len() == 0 { | |
| 359 | + accumulator.Arguments.WriteString("{}") | |
| 387 | 360 | } |
| 388 | 361 | } |
| 389 | 362 | |
| @@ -421,24 +394,35 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina · 31 unmodified lines | ||
| 421 | 394 | } |
| 422 | 395 | |
| 423 | 396 | // Set tool calls if any were accumulated during processing |
| 424 | − if len(toolCallsMap) > 0 { | |
| 425 | − // Convert tool calls map to array, preserving order by index | |
| 426 | − var toolCallsArray []interface{} | |
| 427 | − // Find the maximum index to determine the range | |
| 397 | + if len(toolCallsAccumulator) > 0 { | |
| 398 | + toolCallsCount := 0 | |
| 428 | 399 | maxIndex := -1 |
| 429 | − for index := range toolCallsMap { | |
| 400 | + for index := range toolCallsAccumulator { | |
| 430 | 401 | if index > maxIndex { |
| 431 | 402 | maxIndex = index |
| 432 | 403 | } |
| 433 | 404 | } |
| 434 | − // Iterate through all possible indices up to maxIndex | |
| 405 | + | |
| 435 | 406 | for i := 0; i <= maxIndex; i++ { |
| 436 | − if toolCall, exists := toolCallsMap[i]; exists { | |
| 437 | − toolCallsArray = append(toolCallsArray, toolCall) | |
| 407 | + accumulator, exists := toolCallsAccumulator[i] | |
| 408 | + if !exists { | |
| 409 | + continue | |
| 438 | 410 | } |
| 411 | + | |
| 412 | + arguments := accumulator.Arguments.String() | |
| 413 | + | |
| 414 | + idPath := fmt.Sprintf("choices.0.message.tool_calls.%d.id", toolCallsCount) | |
| 415 | + typePath := fmt.Sprintf("choices.0.message.tool_calls.%d.type", toolCallsCount) | |
| 416 | + namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount) | |
| 417 | + argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount) | |
| 418 | + | |
| 419 | + out, _ = sjson.Set(out, idPath, accumulator.ID) | |
| 420 | + out, _ = sjson.Set(out, typePath, "function") | |
| 421 | + out, _ = sjson.Set(out, namePath, accumulator.Name) | |
| 422 | + out, _ = sjson.Set(out, argumentsPath, arguments) | |
| 423 | + toolCallsCount++ | |
| 439 | 424 | } |
| 440 | − if len(toolCallsArray) > 0 { | |
| 441 | − out, _ = sjson.Set(out, "choices.0.message.tool_calls", toolCallsArray) | |
| 425 | + if toolCallsCount > 0 { | |
| 442 | 426 | out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls") |
| 443 | 427 | } else { |
| 444 | 428 | out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason)) |
| @@ -254,7 +254,10 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte · 253 unmodified lines | ||
| 254 | 254 | toolUse, _ = sjson.Set(toolUse, "id", callID) |
| 255 | 255 | toolUse, _ = sjson.Set(toolUse, "name", name) |
| 256 | 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 | 263 | asst := `{"role":"assistant","content":[]}` |
| @@ -309,16 +312,18 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte · 48 unmodified lines | ||
| 309 | 312 | case gjson.String: |
| 310 | 313 | switch toolChoice.String() { |
| 311 | 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 | 316 | case "none": |
| 314 | 317 | // Leave unset; implies no tools |
| 315 | 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 | 321 | case gjson.JSON: |
| 319 | 322 | if toolChoice.Get("type").String() == "function" { |
| 320 | 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 | 328 | default: |
| 324 | 329 | |
| @@ -344,31 +344,20 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin · 343 unmodified lines | ||
| 344 | 344 | } |
| 345 | 345 | |
| 346 | 346 | // Build response.output from aggregated state |
| 347 | − var outputs []interface{} | |
| 347 | + outputsWrapper := `{"arr":[]}` | |
| 348 | 348 | // reasoning item (if any) |
| 349 | 349 | if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded { |
| 350 | − r := map[string]interface{}{ | |
| 351 | − "id": st.ReasoningItemID, | |
| 352 | − "type": "reasoning", | |
| 353 | − "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}}, | |
| 354 | − } | |
| 355 | − outputs = append(outputs, r) | |
| 350 | + item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` | |
| 351 | + item, _ = sjson.Set(item, "id", st.ReasoningItemID) | |
| 352 | + item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String()) | |
| 353 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 356 | 354 | } |
| 357 | 355 | // assistant message item (if any text) |
| 358 | 356 | if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" { |
| 359 | − m := map[string]interface{}{ | |
| 360 | − "id": st.CurrentMsgID, | |
| 361 | − "type": "message", | |
| 362 | − "status": "completed", | |
| 363 | − "content": []interface{}{map[string]interface{}{ | |
| 364 | − "type": "output_text", | |
| 365 | − "annotations": []interface{}{}, | |
| 366 | − "logprobs": []interface{}{}, | |
| 367 | − "text": st.TextBuf.String(), | |
| 368 | − }}, | |
| 369 | − "role": "assistant", | |
| 370 | − } | |
| 371 | − outputs = append(outputs, m) | |
| 357 | + item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` | |
| 358 | + item, _ = sjson.Set(item, "id", st.CurrentMsgID) | |
| 359 | + item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String()) | |
| 360 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 372 | 361 | } |
| 373 | 362 | // function_call items (in ascending index order for determinism) |
| 374 | 363 | if len(st.FuncArgsBuf) > 0 { |
| @@ -395,19 +384,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin · 20 unmodified lines | ||
| 395 | 384 | if callID == "" && st.CurrentFCID != "" { |
| 396 | 385 | callID = st.CurrentFCID |
| 397 | 386 | } |
| 398 | − item := map[string]interface{}{ | |
| 399 | − "id": fmt.Sprintf("fc_%s", callID), | |
| 400 | − "type": "function_call", | |
| 401 | − "status": "completed", | |
| 402 | − "arguments": args, | |
| 403 | − "call_id": callID, | |
| 404 | − "name": name, | |
| 405 | − } | |
| 406 | − outputs = append(outputs, item) | |
| 387 | + item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` | |
| 388 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) | |
| 389 | + item, _ = sjson.Set(item, "arguments", args) | |
| 390 | + item, _ = sjson.Set(item, "call_id", callID) | |
| 391 | + item, _ = sjson.Set(item, "name", name) | |
| 392 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 407 | 393 | } |
| 408 | 394 | } |
| 409 | − if len(outputs) > 0 { | |
| 410 | − completed, _ = sjson.Set(completed, "response.output", outputs) | |
| 395 | + if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { | |
| 396 | + completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw) | |
| 411 | 397 | } |
| 412 | 398 | |
| 413 | 399 | reasoningTokens := int64(0) |
| @@ -628,27 +614,18 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 214 unmodified lines | ||
| 628 | 614 | } |
| 629 | 615 | |
| 630 | 616 | // Build output array |
| 631 | − var outputs []interface{} | |
| 617 | + outputsWrapper := `{"arr":[]}` | |
| 632 | 618 | if reasoningBuf.Len() > 0 { |
| 633 | − outputs = append(outputs, map[string]interface{}{ | |
| 634 | − "id": reasoningItemID, | |
| 635 | − "type": "reasoning", | |
| 636 | − "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": reasoningBuf.String()}}, | |
| 637 | − }) | |
| 619 | + item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` | |
| 620 | + item, _ = sjson.Set(item, "id", reasoningItemID) | |
| 621 | + item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String()) | |
| 622 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 638 | 623 | } |
| 639 | 624 | if currentMsgID != "" || textBuf.Len() > 0 { |
| 640 | − outputs = append(outputs, map[string]interface{}{ | |
| 641 | − "id": currentMsgID, | |
| 642 | − "type": "message", | |
| 643 | − "status": "completed", | |
| 644 | − "content": []interface{}{map[string]interface{}{ | |
| 645 | − "type": "output_text", | |
| 646 | − "annotations": []interface{}{}, | |
| 647 | − "logprobs": []interface{}{}, | |
| 648 | − "text": textBuf.String(), | |
| 649 | − }}, | |
| 650 | − "role": "assistant", | |
| 651 | − }) | |
| 625 | + item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` | |
| 626 | + item, _ = sjson.Set(item, "id", currentMsgID) | |
| 627 | + item, _ = sjson.Set(item, "content.0.text", textBuf.String()) | |
| 628 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 652 | 629 | } |
| 653 | 630 | if len(toolCalls) > 0 { |
| 654 | 631 | // Preserve index order |
| @@ -669,18 +646,16 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 14 unmodified lines | ||
| 669 | 646 | if args == "" { |
| 670 | 647 | args = "{}" |
| 671 | 648 | } |
| 672 | − outputs = append(outputs, map[string]interface{}{ | |
| 673 | − "id": fmt.Sprintf("fc_%s", st.id), | |
| 674 | − "type": "function_call", | |
| 675 | − "status": "completed", | |
| 676 | − "arguments": args, | |
| 677 | − "call_id": st.id, | |
| 678 | − "name": st.name, | |
| 679 | − }) | |
| 649 | + item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` | |
| 650 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id)) | |
| 651 | + item, _ = sjson.Set(item, "arguments", args) | |
| 652 | + item, _ = sjson.Set(item, "call_id", st.id) | |
| 653 | + item, _ = sjson.Set(item, "name", st.name) | |
| 654 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 680 | 655 | } |
| 681 | 656 | } |
| 682 | − if len(outputs) > 0 { | |
| 683 | − out, _ = sjson.Set(out, "output", outputs) | |
| 657 | + if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { | |
| 658 | + out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw) | |
| 684 | 659 | } |
| 685 | 660 | |
| 686 | 661 | // Usage |
| @@ -9,7 +9,6 @@ package claude · 8 unmodified lines | ||
| 9 | 9 | import ( |
| 10 | 10 | "bytes" |
| 11 | 11 | "context" |
| 12 | − "encoding/json" | |
| 13 | 12 | "fmt" |
| 14 | 13 | "strings" |
| 15 | 14 | |
| @@ -191,21 +190,12 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 175 unmodified lines | ||
| 191 | 190 | return "" |
| 192 | 191 | } |
| 193 | 192 | |
| 194 | − response := map[string]interface{}{ | |
| 195 | − "id": responseData.Get("id").String(), | |
| 196 | − "type": "message", | |
| 197 | − "role": "assistant", | |
| 198 | − "model": responseData.Get("model").String(), | |
| 199 | − "content": []interface{}{}, | |
| 200 | − "stop_reason": nil, | |
| 201 | − "stop_sequence": nil, | |
| 202 | − "usage": map[string]interface{}{ | |
| 203 | − "input_tokens": responseData.Get("usage.input_tokens").Int(), | |
| 204 | − "output_tokens": responseData.Get("usage.output_tokens").Int(), | |
| 205 | − }, | |
| 206 | − } | |
| 193 | + out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` | |
| 194 | + out, _ = sjson.Set(out, "id", responseData.Get("id").String()) | |
| 195 | + out, _ = sjson.Set(out, "model", responseData.Get("model").String()) | |
| 196 | + out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int()) | |
| 197 | + out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int()) | |
| 207 | 198 | |
| 208 | − var contentBlocks []interface{} | |
| 209 | 199 | hasToolCall := false |
| 210 | 200 | |
| 211 | 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 | 236 | if thinkingBuilder.Len() > 0 { |
| 247 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 248 | − "type": "thinking", | |
| 249 | − "thinking": thinkingBuilder.String(), | |
| 250 | − }) | |
| 237 | + block := `{"type":"thinking","thinking":""}` | |
| 238 | + block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) | |
| 239 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 251 | 240 | } |
| 252 | 241 | case "message": |
| 253 | 242 | if content := item.Get("content"); content.Exists() { |
| @@ -256,10 +245,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 2 unmodified lines | ||
| 256 | 245 | if part.Get("type").String() == "output_text" { |
| 257 | 246 | text := part.Get("text").String() |
| 258 | 247 | if text != "" { |
| 259 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 260 | − "type": "text", | |
| 261 | − "text": text, | |
| 262 | − }) | |
| 248 | + block := `{"type":"text","text":""}` | |
| 249 | + block, _ = sjson.Set(block, "text", text) | |
| 250 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 263 | 251 | } |
| 264 | 252 | } |
| 265 | 253 | return true |
| @@ -267,10 +255,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 1 unmodified line | ||
| 267 | 255 | } else { |
| 268 | 256 | text := content.String() |
| 269 | 257 | if text != "" { |
| 270 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 271 | − "type": "text", | |
| 272 | − "text": text, | |
| 273 | − }) | |
| 258 | + block := `{"type":"text","text":""}` | |
| 259 | + block, _ = sjson.Set(block, "text", text) | |
| 260 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 274 | 261 | } |
| 275 | 262 | } |
| 276 | 263 | } |
| @@ -281,54 +268,41 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original · 4 unmodified lines | ||
| 281 | 268 | name = original |
| 282 | 269 | } |
| 283 | 270 | |
| 284 | − toolBlock := map[string]interface{}{ | |
| 285 | − "type": "tool_use", | |
| 286 | − "id": item.Get("call_id").String(), | |
| 287 | − "name": name, | |
| 288 | − "input": map[string]interface{}{}, | |
| 289 | − } | |
| 290 | − | |
| 291 | − if argsStr := item.Get("arguments").String(); argsStr != "" { | |
| 292 | − var args interface{} | |
| 293 | − if err := json.Unmarshal([]byte(argsStr), &args); err == nil { | |
| 294 | − toolBlock["input"] = args | |
| 271 | + toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 272 | + toolBlock, _ = sjson.Set(toolBlock, "id", item.Get("call_id").String()) | |
| 273 | + toolBlock, _ = sjson.Set(toolBlock, "name", name) | |
| 274 | + inputRaw := "{}" | |
| 275 | + if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) { | |
| 276 | + argsJSON := gjson.Parse(argsStr) | |
| 277 | + if argsJSON.IsObject() { | |
| 278 | + inputRaw = argsJSON.Raw | |
| 295 | 279 | } |
| 296 | 280 | } |
| 297 | − | |
| 298 | − contentBlocks = append(contentBlocks, toolBlock) | |
| 281 | + toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) | |
| 282 | + out, _ = sjson.SetRaw(out, "content.-1", toolBlock) | |
| 299 | 283 | } |
| 300 | 284 | return true |
| 301 | 285 | }) |
| 302 | 286 | } |
| 303 | 287 | |
| 304 | − if len(contentBlocks) > 0 { | |
| 305 | − response["content"] = contentBlocks | |
| 306 | − } | |
| 307 | − | |
| 308 | 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 | 290 | } else if hasToolCall { |
| 311 | − response["stop_reason"] = "tool_use" | |
| 291 | + out, _ = sjson.Set(out, "stop_reason", "tool_use") | |
| 312 | 292 | } else { |
| 313 | − response["stop_reason"] = "end_turn" | |
| 293 | + out, _ = sjson.Set(out, "stop_reason", "end_turn") | |
| 314 | 294 | } |
| 315 | 295 | |
| 316 | 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 | 300 | if responseData.Get("usage.input_tokens").Exists() || responseData.Get("usage.output_tokens").Exists() { |
| 321 | − response["usage"] = map[string]interface{}{ | |
| 322 | − "input_tokens": responseData.Get("usage.input_tokens").Int(), | |
| 323 | − "output_tokens": responseData.Get("usage.output_tokens").Int(), | |
| 324 | − } | |
| 301 | + out, _ = sjson.Set(out, "usage.input_tokens", responseData.Get("usage.input_tokens").Int()) | |
| 302 | + out, _ = sjson.Set(out, "usage.output_tokens", responseData.Get("usage.output_tokens").Int()) | |
| 325 | 303 | } |
| 326 | 304 | |
| 327 | − responseJSON, err := json.Marshal(response) | |
| 328 | − if err != nil { | |
| 329 | − return "" | |
| 330 | − } | |
| 331 | − return string(responseJSON) | |
| 305 | + return out | |
| 332 | 306 | } |
| 333 | 307 | |
| 334 | 308 | // buildReverseMapFromClaudeOriginalShortToOriginal builds a map[short]original from original Claude request tools. |
| @@ -7,7 +7,6 @@ package gemini · 6 unmodified lines | ||
| 7 | 7 | import ( |
| 8 | 8 | "bytes" |
| 9 | 9 | "context" |
| 10 | − "encoding/json" | |
| 11 | 10 | "fmt" |
| 12 | 11 | "time" |
| 13 | 12 | |
| @@ -190,19 +189,19 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 176 unmodified lines | ||
| 190 | 189 | } |
| 191 | 190 | |
| 192 | 191 | // Process output content to build parts array |
| 193 | − var parts []interface{} | |
| 194 | 192 | hasToolCall := false |
| 195 | − var pendingFunctionCalls []interface{} | |
| 193 | + var pendingFunctionCalls []string | |
| 196 | 194 | |
| 197 | 195 | flushPendingFunctionCalls := func() { |
| 198 | − if len(pendingFunctionCalls) > 0 { | |
| 199 | − // Add all pending function calls as individual parts | |
| 200 | − // This maintains the original Gemini API format while ensuring consecutive calls are grouped together | |
| 201 | − for _, fc := range pendingFunctionCalls { | |
| 202 | − parts = append(parts, fc) | |
| 203 | − } | |
| 204 | − pendingFunctionCalls = nil | |
| 196 | + if len(pendingFunctionCalls) == 0 { | |
| 197 | + return | |
| 198 | + } | |
| 199 | + // Add all pending function calls as individual parts | |
| 200 | + // This maintains the original Gemini API format while ensuring consecutive calls are grouped together | |
| 201 | + for _, fc := range pendingFunctionCalls { | |
| 202 | + template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", fc) | |
| 205 | 203 | } |
| 204 | + pendingFunctionCalls = nil | |
| 206 | 205 | } |
| 207 | 206 | |
| 208 | 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 | 216 | // Add thinking content |
| 218 | 217 | if content := value.Get("content"); content.Exists() { |
| 219 | − part := map[string]interface{}{ | |
| 220 | − "thought": true, | |
| 221 | − "text": content.String(), | |
| 222 | − } | |
| 223 | − parts = append(parts, part) | |
| 218 | + part := `{"text":"","thought":true}` | |
| 219 | + part, _ = sjson.Set(part, "text", content.String()) | |
| 220 | + template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) | |
| 224 | 221 | } |
| 225 | 222 | |
| 226 | 223 | case "message": |
| @@ -232,10 +229,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 5 unmodified lines | ||
| 232 | 229 | content.ForEach(func(_, contentItem gjson.Result) bool { |
| 233 | 230 | if contentItem.Get("type").String() == "output_text" { |
| 234 | 231 | if text := contentItem.Get("text"); text.Exists() { |
| 235 | − part := map[string]interface{}{ | |
| 236 | − "text": text.String(), | |
| 237 | − } | |
| 238 | − parts = append(parts, part) | |
| 232 | + part := `{"text":""}` | |
| 233 | + part, _ = sjson.Set(part, "text", text.String()) | |
| 234 | + template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) | |
| 239 | 235 | } |
| 240 | 236 | } |
| 241 | 237 | return true |
| @@ -245,28 +241,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 3 unmodified lines | ||
| 245 | 241 | case "function_call": |
| 246 | 242 | // Collect function call for potential merging with consecutive ones |
| 247 | 243 | hasToolCall = true |
| 248 | − functionCall := map[string]interface{}{ | |
| 249 | − "functionCall": map[string]interface{}{ | |
| 250 | − "name": func() string { | |
| 251 | − n := value.Get("name").String() | |
| 252 | − rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON) | |
| 253 | − if orig, ok := rev[n]; ok { | |
| 254 | − return orig | |
| 255 | − } | |
| 256 | − return n | |
| 257 | − }(), | |
| 258 | − "args": map[string]interface{}{}, | |
| 259 | − }, | |
| 244 | + functionCall := `{"functionCall":{"args":{},"name":""}}` | |
| 245 | + { | |
| 246 | + n := value.Get("name").String() | |
| 247 | + rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON) | |
| 248 | + if orig, ok := rev[n]; ok { | |
| 249 | + n = orig | |
| 250 | + } | |
| 251 | + functionCall, _ = sjson.Set(functionCall, "functionCall.name", n) | |
| 260 | 252 | } |
| 261 | 253 | |
| 262 | 254 | // Parse and set arguments |
| 263 | 255 | if argsStr := value.Get("arguments").String(); argsStr != "" { |
| 264 | 256 | argsResult := gjson.Parse(argsStr) |
| 265 | 257 | if argsResult.IsObject() { |
| 266 | − var args map[string]interface{} | |
| 267 | − if err := json.Unmarshal([]byte(argsStr), &args); err == nil { | |
| 268 | − functionCall["functionCall"].(map[string]interface{})["args"] = args | |
| 269 | − } | |
| 258 | + functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr) | |
| 270 | 259 | } |
| 271 | 260 | } |
| 272 | 261 | |
| @@ -279,11 +268,6 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, · 6 unmodified lines | ||
| 279 | 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 | 271 | // Set finish reason based on whether there were tool calls |
| 288 | 272 | if hasToolCall { |
| 289 | 273 | template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") |
| @@ -323,15 +307,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { · 33 unmodified lines | ||
| 323 | 307 | return rev |
| 324 | 308 | } |
| 325 | 309 | |
| 326 | −// mustMarshalJSON marshals a value to JSON, panicking on error. | |
| 327 | −func mustMarshalJSON(v interface{}) string { | |
| 328 | − data, err := json.Marshal(v) | |
| 329 | − if err != nil { | |
| 330 | − return "" | |
| 331 | − } | |
| 332 | − return string(data) | |
| 333 | −} | |
| 334 | − | |
| 335 | 310 | func GeminiTokenCount(ctx context.Context, count int64) string { |
| 336 | 311 | return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) |
| 337 | 312 | } |
| @@ -7,10 +7,8 @@ package claude · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | − "encoding/json" | |
| 11 | 10 | "strings" |
| 12 | 11 | |
| 13 | − client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" | |
| 14 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" |
| 15 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| 16 | 14 | "github.com/tidwall/gjson" |
| @@ -41,92 +39,100 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 24 unmodified lines | ||
| 41 | 39 | rawJSON := bytes.Clone(inputRawJSON) |
| 42 | 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 | 46 | // system instruction |
| 45 | − var systemInstruction *client.Content | |
| 46 | − systemResult := gjson.GetBytes(rawJSON, "system") | |
| 47 | − if systemResult.IsArray() { | |
| 48 | − systemResults := systemResult.Array() | |
| 49 | − systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}} | |
| 50 | − for i := 0; i < len(systemResults); i++ { | |
| 51 | − systemPromptResult := systemResults[i] | |
| 52 | − systemTypePromptResult := systemPromptResult.Get("type") | |
| 53 | − if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" { | |
| 54 | − systemPrompt := systemPromptResult.Get("text").String() | |
| 55 | − systemPart := client.Part{Text: systemPrompt} | |
| 56 | − systemInstruction.Parts = append(systemInstruction.Parts, systemPart) | |
| 47 | + if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() { | |
| 48 | + systemInstruction := `{"role":"user","parts":[]}` | |
| 49 | + hasSystemParts := false | |
| 50 | + systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool { | |
| 51 | + if systemPromptResult.Get("type").String() == "text" { | |
| 52 | + textResult := systemPromptResult.Get("text") | |
| 53 | + if textResult.Type == gjson.String { | |
| 54 | + part := `{"text":""}` | |
| 55 | + part, _ = sjson.Set(part, "text", textResult.String()) | |
| 56 | + systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part) | |
| 57 | + hasSystemParts = true | |
| 58 | + } | |
| 57 | 59 | } |
| 58 | − } | |
| 59 | − if len(systemInstruction.Parts) == 0 { | |
| 60 | − systemInstruction = nil | |
| 60 | + return true | |
| 61 | + }) | |
| 62 | + if hasSystemParts { | |
| 63 | + out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction) | |
| 61 | 64 | } |
| 62 | 65 | } |
| 63 | 66 | |
| 64 | 67 | // contents |
| 65 | − contents := make([]client.Content, 0) | |
| 66 | − messagesResult := gjson.GetBytes(rawJSON, "messages") | |
| 67 | − if messagesResult.IsArray() { | |
| 68 | − messageResults := messagesResult.Array() | |
| 69 | − for i := 0; i < len(messageResults); i++ { | |
| 70 | − messageResult := messageResults[i] | |
| 68 | + if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() { | |
| 69 | + messagesResult.ForEach(func(_, messageResult gjson.Result) bool { | |
| 71 | 70 | roleResult := messageResult.Get("role") |
| 72 | 71 | if roleResult.Type != gjson.String { |
| 73 | − continue | |
| 72 | + return true | |
| 74 | 73 | } |
| 75 | 74 | role := roleResult.String() |
| 76 | 75 | if role == "assistant" { |
| 77 | 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 | 82 | contentsResult := messageResult.Get("content") |
| 81 | 83 | if contentsResult.IsArray() { |
| 82 | − contentResults := contentsResult.Array() | |
| 83 | − for j := 0; j < len(contentResults); j++ { | |
| 84 | − contentResult := contentResults[j] | |
| 85 | − contentTypeResult := contentResult.Get("type") | |
| 86 | − if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { | |
| 87 | − prompt := contentResult.Get("text").String() | |
| 88 | − clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt}) | |
| 89 | − } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" { | |
| 84 | + contentsResult.ForEach(func(_, contentResult gjson.Result) bool { | |
| 85 | + switch contentResult.Get("type").String() { | |
| 86 | + case "text": | |
| 87 | + part := `{"text":""}` | |
| 88 | + part, _ = sjson.Set(part, "text", contentResult.Get("text").String()) | |
| 89 | + contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) | |
| 90 | + | |
| 91 | + case "tool_use": | |
| 90 | 92 | functionName := contentResult.Get("name").String() |
| 91 | 93 | functionArgs := contentResult.Get("input").String() |
| 92 | − var args map[string]any | |
| 93 | − if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { | |
| 94 | − clientContent.Parts = append(clientContent.Parts, client.Part{ | |
| 95 | − FunctionCall: &client.FunctionCall{Name: functionName, Args: args}, | |
| 96 | − ThoughtSignature: geminiCLIClaudeThoughtSignature, | |
| 97 | − }) | |
| 94 | + argsResult := gjson.Parse(functionArgs) | |
| 95 | + if argsResult.IsObject() && gjson.Valid(functionArgs) { | |
| 96 | + part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}` | |
| 97 | + part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature) | |
| 98 | + part, _ = sjson.Set(part, "functionCall.name", functionName) | |
| 99 | + part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs) | |
| 100 | + contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) | |
| 98 | 101 | } |
| 99 | − } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { | |
| 102 | + | |
| 103 | + case "tool_result": | |
| 100 | 104 | toolCallID := contentResult.Get("tool_use_id").String() |
| 101 | − if toolCallID != "" { | |
| 102 | − funcName := toolCallID | |
| 103 | − toolCallIDs := strings.Split(toolCallID, "-") | |
| 104 | − if len(toolCallIDs) > 1 { | |
| 105 | − funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-") | |
| 106 | − } | |
| 107 | − responseData := contentResult.Get("content").Raw | |
| 108 | − functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}} | |
| 109 | − clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse}) | |
| 105 | + if toolCallID == "" { | |
| 106 | + return true | |
| 107 | + } | |
| 108 | + funcName := toolCallID | |
| 109 | + toolCallIDs := strings.Split(toolCallID, "-") | |
| 110 | + if len(toolCallIDs) > 1 { | |
| 111 | + funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-") | |
| 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 | − } | |
| 113 | − contents = append(contents, clientContent) | |
| 119 | + return true | |
| 120 | + }) | |
| 121 | + out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON) | |
| 114 | 122 | } else if contentsResult.Type == gjson.String { |
| 115 | − prompt := contentsResult.String() | |
| 116 | − contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}}) | |
| 123 | + part := `{"text":""}` | |
| 124 | + part, _ = sjson.Set(part, "text", contentsResult.String()) | |
| 125 | + contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) | |
| 126 | + out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON) | |
| 117 | 127 | } |
| 118 | − } | |
| 128 | + return true | |
| 129 | + }) | |
| 119 | 130 | } |
| 120 | 131 | |
| 121 | 132 | // tools |
| 122 | − var tools []client.ToolDeclaration | |
| 123 | − toolsResult := gjson.GetBytes(rawJSON, "tools") | |
| 124 | − if toolsResult.IsArray() { | |
| 125 | − tools = make([]client.ToolDeclaration, 1) | |
| 126 | − tools[0].FunctionDeclarations = make([]any, 0) | |
| 127 | − toolsResults := toolsResult.Array() | |
| 128 | − for i := 0; i < len(toolsResults); i++ { | |
| 129 | − toolResult := toolsResults[i] | |
| 133 | + if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() { | |
| 134 | + hasTools := false | |
| 135 | + toolsResult.ForEach(func(_, toolResult gjson.Result) bool { | |
| 130 | 136 | inputSchemaResult := toolResult.Get("input_schema") |
| 131 | 137 | if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { |
| 132 | 138 | inputSchema := inputSchemaResult.Raw |
| @@ -136,30 +142,19 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] · 3 unmodified lines | ||
| 136 | 142 | tool, _ = sjson.Delete(tool, "input_examples") |
| 137 | 143 | tool, _ = sjson.Delete(tool, "type") |
| 138 | 144 | tool, _ = sjson.Delete(tool, "cache_control") |
| 139 | − var toolDeclaration any | |
| 140 | − if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil { | |
| 141 | − tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration) | |
| 145 | + if gjson.Valid(tool) && gjson.Parse(tool).IsObject() { | |
| 146 | + if !hasTools { | |
| 147 | + out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`) | |
| 148 | + hasTools = true | |
| 149 | + } | |
| 150 | + out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool) | |
| 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 | 160 | // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when type==enabled |
| @@ -9,7 +9,6 @@ package claude · 8 unmodified lines | ||
| 9 | 9 | import ( |
| 10 | 10 | "bytes" |
| 11 | 11 | "context" |
| 12 | − "encoding/json" | |
| 13 | 12 | "fmt" |
| 14 | 13 | "strings" |
| 15 | 14 | "sync/atomic" |
| @@ -276,22 +275,16 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 260 unmodified lines | ||
| 276 | 275 | |
| 277 | 276 | root := gjson.ParseBytes(rawJSON) |
| 278 | 277 | |
| 279 | − response := map[string]interface{}{ | |
| 280 | − "id": root.Get("response.responseId").String(), | |
| 281 | − "type": "message", | |
| 282 | − "role": "assistant", | |
| 283 | − "model": root.Get("response.modelVersion").String(), | |
| 284 | − "content": []interface{}{}, | |
| 285 | − "stop_reason": nil, | |
| 286 | − "stop_sequence": nil, | |
| 287 | − "usage": map[string]interface{}{ | |
| 288 | − "input_tokens": root.Get("response.usageMetadata.promptTokenCount").Int(), | |
| 289 | − "output_tokens": root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int(), | |
| 290 | − }, | |
| 291 | − } | |
| 278 | + out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` | |
| 279 | + out, _ = sjson.Set(out, "id", root.Get("response.responseId").String()) | |
| 280 | + out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String()) | |
| 281 | + | |
| 282 | + inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int() | |
| 283 | + outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int() | |
| 284 | + out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) | |
| 285 | + out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) | |
| 292 | 286 | |
| 293 | 287 | parts := root.Get("response.candidates.0.content.parts") |
| 294 | − var contentBlocks []interface{} | |
| 295 | 288 | textBuilder := strings.Builder{} |
| 296 | 289 | thinkingBuilder := strings.Builder{} |
| 297 | 290 | toolIDCounter := 0 |
| @@ -301,10 +294,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 3 unmodified lines | ||
| 301 | 294 | if textBuilder.Len() == 0 { |
| 302 | 295 | return |
| 303 | 296 | } |
| 304 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 305 | − "type": "text", | |
| 306 | − "text": textBuilder.String(), | |
| 307 | − }) | |
| 297 | + block := `{"type":"text","text":""}` | |
| 298 | + block, _ = sjson.Set(block, "text", textBuilder.String()) | |
| 299 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 308 | 300 | textBuilder.Reset() |
| 309 | 301 | } |
| 310 | 302 | |
| @@ -312,10 +304,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 1 unmodified line | ||
| 312 | 304 | if thinkingBuilder.Len() == 0 { |
| 313 | 305 | return |
| 314 | 306 | } |
| 315 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 316 | − "type": "thinking", | |
| 317 | − "thinking": thinkingBuilder.String(), | |
| 318 | − }) | |
| 307 | + block := `{"type":"thinking","thinking":""}` | |
| 308 | + block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) | |
| 309 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 319 | 310 | thinkingBuilder.Reset() |
| 320 | 311 | } |
| 321 | 312 | |
| @@ -339,21 +330,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 17 unmodified lines | ||
| 339 | 330 | |
| 340 | 331 | name := functionCall.Get("name").String() |
| 341 | 332 | toolIDCounter++ |
| 342 | − toolBlock := map[string]interface{}{ | |
| 343 | − "type": "tool_use", | |
| 344 | − "id": fmt.Sprintf("tool_%d", toolIDCounter), | |
| 345 | − "name": name, | |
| 346 | − "input": map[string]interface{}{}, | |
| 347 | − } | |
| 348 | − | |
| 349 | − if args := functionCall.Get("args"); args.Exists() { | |
| 350 | − var parsed interface{} | |
| 351 | − if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil { | |
| 352 | − toolBlock["input"] = parsed | |
| 353 | − } | |
| 333 | + toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 334 | + toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) | |
| 335 | + toolBlock, _ = sjson.Set(toolBlock, "name", name) | |
| 336 | + inputRaw := "{}" | |
| 337 | + if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() { | |
| 338 | + inputRaw = args.Raw | |
| 354 | 339 | } |
| 355 | − | |
| 356 | − contentBlocks = append(contentBlocks, toolBlock) | |
| 340 | + toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) | |
| 341 | + out, _ = sjson.SetRaw(out, "content.-1", toolBlock) | |
| 357 | 342 | continue |
| 358 | 343 | } |
| 359 | 344 | } |
| @@ -362,8 +347,6 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig · 2 unmodified lines | ||
| 362 | 347 | flushThinking() |
| 363 | 348 | flushText() |
| 364 | 349 | |
| 365 | − response["content"] = contentBlocks | |
| 366 | − | |
| 367 | 350 | stopReason := "end_turn" |
| 368 | 351 | if hasToolCall { |
| 369 | 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"] = stopReason | |
| 365 | + 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) { | |
| 385 | − if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() { | |
| 386 | − delete(response, "usage") | |
| 387 | − } | |
| 367 | + if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() { | |
| 368 | + out, _ = sjson.Delete(out, "usage") | |
| 388 | 369 | } |
| 389 | 370 | |
| 390 | − encoded, err := json.Marshal(response) | |
| 391 | − if err != nil { | |
| 392 | − return "" | |
| 393 | − } | |
| 394 | − return string(encoded) | |
| 371 | + return out | |
| 395 | 372 | } |
| 396 | 373 | |
| 397 | 374 | func ClaudeTokenCount(ctx context.Context, count int64) string { |
| @@ -7,7 +7,6 @@ package gemini · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | − "encoding/json" | |
| 11 | 10 | "fmt" |
| 12 | 11 | |
| 13 | 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 | 117 | // FunctionCallGroup represents a group of function calls and their responses |
| 119 | 118 | type FunctionCallGroup struct { |
| 120 | − ModelContent map[string]interface{} | |
| 121 | − FunctionCalls []gjson.Result | |
| 122 | 119 | ResponsesNeeded int |
| 123 | 120 | } |
| 124 | 121 | |
| @@ -146,7 +143,7 @@ func fixCLIToolResponse(input string) (string, error) { · 21 unmodified lines | ||
| 146 | 143 | } |
| 147 | 144 | |
| 148 | 145 | // Initialize data structures for processing and grouping |
| 149 | − var newContents []interface{} // Final processed contents array | |
| 146 | + contentsWrapper := `{"contents":[]}` | |
| 150 | 147 | var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses |
| 151 | 148 | 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 | 175 | collectedResponses = collectedResponses[group.ResponsesNeeded:] |
| 179 | 176 | |
| 180 | 177 | // Create merged function response content |
| 181 | − var responseParts []interface{} | |
| 178 | + functionResponseContent := `{"parts":[],"role":"function"}` | |
| 182 | 179 | for _, response := range groupResponses { |
| 183 | − var responseMap map[string]interface{} | |
| 184 | − errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap) | |
| 185 | − if errUnmarshal != nil { | |
| 186 | − log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal) | |
| 180 | + if !response.IsObject() { | |
| 181 | + log.Warnf("failed to parse function response") | |
| 187 | 182 | 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 { | |
| 193 | − functionResponseContent := map[string]interface{}{ | |
| 194 | − "parts": responseParts, | |
| 195 | − "role": "function", | |
| 196 | − } | |
| 197 | − newContents = append(newContents, functionResponseContent) | |
| 187 | + if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { | |
| 188 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) | |
| 198 | 189 | } |
| 199 | 190 | |
| 200 | 191 | // Remove this group as it's been satisfied |
| @@ -208,50 +199,42 @@ func fixCLIToolResponse(input string) (string, error) { · 7 unmodified lines | ||
| 208 | 199 | |
| 209 | 200 | // If this is a model with function calls, create a new group |
| 210 | 201 | if role == "model" { |
| 211 | − var functionCallsInThisModel []gjson.Result | |
| 202 | + functionCallsCount := 0 | |
| 212 | 203 | parts.ForEach(func(_, part gjson.Result) bool { |
| 213 | 204 | if part.Get("functionCall").Exists() { |
| 214 | − functionCallsInThisModel = append(functionCallsInThisModel, part) | |
| 205 | + functionCallsCount++ | |
| 215 | 206 | } |
| 216 | 207 | return true |
| 217 | 208 | }) |
| 218 | 209 | |
| 219 | − if len(functionCallsInThisModel) > 0 { | |
| 210 | + if functionCallsCount > 0 { | |
| 220 | 211 | // Add the model content |
| 221 | − var contentMap map[string]interface{} | |
| 222 | − errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap) | |
| 223 | − if errUnmarshal != nil { | |
| 224 | − log.Warnf("failed to unmarshal model content: %v\n", errUnmarshal) | |
| 212 | + if !value.IsObject() { | |
| 213 | + log.Warnf("failed to parse model content") | |
| 225 | 214 | return true |
| 226 | 215 | } |
| 227 | − newContents = append(newContents, contentMap) | |
| 216 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) | |
| 228 | 217 | |
| 229 | 218 | // Create a new group for tracking responses |
| 230 | 219 | group := &FunctionCallGroup{ |
| 231 | − ModelContent: contentMap, | |
| 232 | − FunctionCalls: functionCallsInThisModel, | |
| 233 | − ResponsesNeeded: len(functionCallsInThisModel), | |
| 220 | + ResponsesNeeded: functionCallsCount, | |
| 234 | 221 | } |
| 235 | 222 | pendingGroups = append(pendingGroups, group) |
| 236 | 223 | } else { |
| 237 | 224 | // Regular model content without function calls |
| 238 | − var contentMap map[string]interface{} | |
| 239 | − errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap) | |
| 240 | − if errUnmarshal != nil { | |
| 241 | − log.Warnf("failed to unmarshal content: %v\n", errUnmarshal) | |
| 225 | + if !value.IsObject() { | |
| 226 | + log.Warnf("failed to parse content") | |
| 242 | 227 | return true |
| 243 | 228 | } |
| 244 | − newContents = append(newContents, contentMap) | |
| 229 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) | |
| 245 | 230 | } |
| 246 | 231 | } else { |
| 247 | 232 | // Non-model content (user, etc.) |
| 248 | − var contentMap map[string]interface{} | |
| 249 | − errUnmarshal := json.Unmarshal([]byte(value.Raw), &contentMap) | |
| 250 | − if errUnmarshal != nil { | |
| 251 | − log.Warnf("failed to unmarshal content: %v\n", errUnmarshal) | |
| 233 | + if !value.IsObject() { | |
| 234 | + log.Warnf("failed to parse content") | |
| 252 | 235 | return true |
| 253 | 236 | } |
| 254 | − newContents = append(newContents, contentMap) | |
| 237 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) | |
| 255 | 238 | } |
| 256 | 239 | |
| 257 | 240 | return true |
| @@ -263,31 +246,24 @@ func fixCLIToolResponse(input string) (string, error) { · 5 unmodified lines | ||
| 263 | 246 | groupResponses := collectedResponses[:group.ResponsesNeeded] |
| 264 | 247 | collectedResponses = collectedResponses[group.ResponsesNeeded:] |
| 265 | 248 | |
| 266 | − var responseParts []interface{} | |
| 249 | + functionResponseContent := `{"parts":[],"role":"function"}` | |
| 267 | 250 | for _, response := range groupResponses { |
| 268 | − var responseMap map[string]interface{} | |
| 269 | − errUnmarshal := json.Unmarshal([]byte(response.Raw), &responseMap) | |
| 270 | − if errUnmarshal != nil { | |
| 271 | − log.Warnf("failed to unmarshal function response: %v\n", errUnmarshal) | |
| 251 | + if !response.IsObject() { | |
| 252 | + log.Warnf("failed to parse function response") | |
| 272 | 253 | 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 { | |
| 278 | − functionResponseContent := map[string]interface{}{ | |
| 279 | − "parts": responseParts, | |
| 280 | − "role": "function", | |
| 281 | − } | |
| 282 | − newContents = append(newContents, functionResponseContent) | |
| 258 | + if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { | |
| 259 | + contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) | |
| 283 | 260 | } |
| 284 | 261 | } |
| 285 | 262 | } |
| 286 | 263 | |
| 287 | 264 | // Update the original JSON with the new contents |
| 288 | 265 | result := input |
| 289 | − newContentsJSON, _ := json.Marshal(newContents) | |
| 290 | − result, _ = sjson.Set(result, "request.contents", json.RawMessage(newContentsJSON)) | |
| 266 | + result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw) | |
| 291 | 267 | |
| 292 | 268 | return result, nil |
| 293 | 269 | } |
| @@ -278,7 +278,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 277 unmodified lines | ||
| 278 | 278 | log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) |
| 279 | 279 | continue |
| 280 | 280 | } |
| 281 | − fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{}) | |
| 281 | + fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) | |
| 282 | 282 | if errSet != nil { |
| 283 | 283 | log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) |
| 284 | 284 | continue |
| @@ -293,7 +293,7 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo · 8 unmodified lines | ||
| 293 | 293 | log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) |
| 294 | 294 | continue |
| 295 | 295 | } |
| 296 | − fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{}) | |
| 296 | + fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) | |
| 297 | 297 | if errSet != nil { |
| 298 | 298 | log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) |
| 299 | 299 | continue |
| @@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "context" |
| 11 | − "encoding/json" | |
| 12 | 11 | "fmt" |
| 13 | 12 | "strings" |
| 14 | 13 | "sync/atomic" |
| @@ -171,21 +170,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ · 156 unmodified lines | ||
| 171 | 170 | mimeType = "image/png" |
| 172 | 171 | } |
| 173 | 172 | imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) |
| 174 | − imagePayload, err := json.Marshal(map[string]any{ | |
| 175 | − "type": "image_url", | |
| 176 | − "image_url": map[string]string{ | |
| 177 | − "url": imageURL, | |
| 178 | − }, | |
| 179 | − }) | |
| 180 | − if err != nil { | |
| 181 | − continue | |
| 182 | − } | |
| 173 | + imagePayload := `{"image_url":{"url":""},"type":"image_url"}` | |
| 174 | + imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) | |
| 183 | 175 | imagesResult := gjson.Get(template, "choices.0.delta.images") |
| 184 | 176 | if !imagesResult.Exists() || !imagesResult.IsArray() { |
| 185 | 177 | template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`) |
| 186 | 178 | } |
| 187 | 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 | } |
| @@ -7,10 +7,8 @@ package claude · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | − "encoding/json" | |
| 11 | 10 | "strings" |
| 12 | 11 | |
| 13 | − client "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" | |
| 14 | 12 | "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/gemini/common" |
| 15 | 13 | "github.com/router-for-me/CLIProxyAPI/v6/internal/util" |
| 16 | 14 | "github.com/tidwall/gjson" |
| @@ -34,92 +32,100 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 17 unmodified lines | ||
| 34 | 32 | rawJSON := bytes.Clone(inputRawJSON) |
| 35 | 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 | 39 | // system instruction |
| 38 | − var systemInstruction *client.Content | |
| 39 | − systemResult := gjson.GetBytes(rawJSON, "system") | |
| 40 | − if systemResult.IsArray() { | |
| 41 | − systemResults := systemResult.Array() | |
| 42 | − systemInstruction = &client.Content{Role: "user", Parts: []client.Part{}} | |
| 43 | − for i := 0; i < len(systemResults); i++ { | |
| 44 | − systemPromptResult := systemResults[i] | |
| 45 | − systemTypePromptResult := systemPromptResult.Get("type") | |
| 46 | − if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" { | |
| 47 | − systemPrompt := systemPromptResult.Get("text").String() | |
| 48 | − systemPart := client.Part{Text: systemPrompt} | |
| 49 | − systemInstruction.Parts = append(systemInstruction.Parts, systemPart) | |
| 40 | + if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() { | |
| 41 | + systemInstruction := `{"role":"user","parts":[]}` | |
| 42 | + hasSystemParts := false | |
| 43 | + systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool { | |
| 44 | + if systemPromptResult.Get("type").String() == "text" { | |
| 45 | + textResult := systemPromptResult.Get("text") | |
| 46 | + if textResult.Type == gjson.String { | |
| 47 | + part := `{"text":""}` | |
| 48 | + part, _ = sjson.Set(part, "text", textResult.String()) | |
| 49 | + systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part) | |
| 50 | + hasSystemParts = true | |
| 51 | + } | |
| 50 | 52 | } |
| 51 | − } | |
| 52 | − if len(systemInstruction.Parts) == 0 { | |
| 53 | − systemInstruction = nil | |
| 53 | + return true | |
| 54 | + }) | |
| 55 | + if hasSystemParts { | |
| 56 | + out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction) | |
| 54 | 57 | } |
| 55 | 58 | } |
| 56 | 59 | |
| 57 | 60 | // contents |
| 58 | − contents := make([]client.Content, 0) | |
| 59 | − messagesResult := gjson.GetBytes(rawJSON, "messages") | |
| 60 | − if messagesResult.IsArray() { | |
| 61 | − messageResults := messagesResult.Array() | |
| 62 | − for i := 0; i < len(messageResults); i++ { | |
| 63 | − messageResult := messageResults[i] | |
| 61 | + if messagesResult := gjson.GetBytes(rawJSON, "messages"); messagesResult.IsArray() { | |
| 62 | + messagesResult.ForEach(func(_, messageResult gjson.Result) bool { | |
| 64 | 63 | roleResult := messageResult.Get("role") |
| 65 | 64 | if roleResult.Type != gjson.String { |
| 66 | − continue | |
| 65 | + return true | |
| 67 | 66 | } |
| 68 | 67 | role := roleResult.String() |
| 69 | 68 | if role == "assistant" { |
| 70 | 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 | 75 | contentsResult := messageResult.Get("content") |
| 74 | 76 | if contentsResult.IsArray() { |
| 75 | − contentResults := contentsResult.Array() | |
| 76 | − for j := 0; j < len(contentResults); j++ { | |
| 77 | − contentResult := contentResults[j] | |
| 78 | − contentTypeResult := contentResult.Get("type") | |
| 79 | − if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { | |
| 80 | − prompt := contentResult.Get("text").String() | |
| 81 | − clientContent.Parts = append(clientContent.Parts, client.Part{Text: prompt}) | |
| 82 | − } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" { | |
| 77 | + contentsResult.ForEach(func(_, contentResult gjson.Result) bool { | |
| 78 | + switch contentResult.Get("type").String() { | |
| 79 | + case "text": | |
| 80 | + part := `{"text":""}` | |
| 81 | + part, _ = sjson.Set(part, "text", contentResult.Get("text").String()) | |
| 82 | + contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) | |
| 83 | + | |
| 84 | + case "tool_use": | |
| 83 | 85 | functionName := contentResult.Get("name").String() |
| 84 | 86 | functionArgs := contentResult.Get("input").String() |
| 85 | − var args map[string]any | |
| 86 | − if err := json.Unmarshal([]byte(functionArgs), &args); err == nil { | |
| 87 | − clientContent.Parts = append(clientContent.Parts, client.Part{ | |
| 88 | − FunctionCall: &client.FunctionCall{Name: functionName, Args: args}, | |
| 89 | − ThoughtSignature: geminiClaudeThoughtSignature, | |
| 90 | − }) | |
| 87 | + argsResult := gjson.Parse(functionArgs) | |
| 88 | + if argsResult.IsObject() && gjson.Valid(functionArgs) { | |
| 89 | + part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}` | |
| 90 | + part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature) | |
| 91 | + part, _ = sjson.Set(part, "functionCall.name", functionName) | |
| 92 | + part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs) | |
| 93 | + contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) | |
| 91 | 94 | } |
| 92 | − } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { | |
| 95 | + | |
| 96 | + case "tool_result": | |
| 93 | 97 | toolCallID := contentResult.Get("tool_use_id").String() |
| 94 | − if toolCallID != "" { | |
| 95 | − funcName := toolCallID | |
| 96 | − toolCallIDs := strings.Split(toolCallID, "-") | |
| 97 | − if len(toolCallIDs) > 1 { | |
| 98 | − funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-") | |
| 99 | − } | |
| 100 | − responseData := contentResult.Get("content").Raw | |
| 101 | − functionResponse := client.FunctionResponse{Name: funcName, Response: map[string]interface{}{"result": responseData}} | |
| 102 | − clientContent.Parts = append(clientContent.Parts, client.Part{FunctionResponse: &functionResponse}) | |
| 98 | + if toolCallID == "" { | |
| 99 | + return true | |
| 100 | + } | |
| 101 | + funcName := toolCallID | |
| 102 | + toolCallIDs := strings.Split(toolCallID, "-") | |
| 103 | + if len(toolCallIDs) > 1 { | |
| 104 | + funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-") | |
| 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 | − } | |
| 106 | − contents = append(contents, clientContent) | |
| 112 | + return true | |
| 113 | + }) | |
| 114 | + out, _ = sjson.SetRaw(out, "contents.-1", contentJSON) | |
| 107 | 115 | } else if contentsResult.Type == gjson.String { |
| 108 | − prompt := contentsResult.String() | |
| 109 | − contents = append(contents, client.Content{Role: role, Parts: []client.Part{{Text: prompt}}}) | |
| 116 | + part := `{"text":""}` | |
| 117 | + part, _ = sjson.Set(part, "text", contentsResult.String()) | |
| 118 | + contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) | |
| 119 | + out, _ = sjson.SetRaw(out, "contents.-1", contentJSON) | |
| 110 | 120 | } |
| 111 | − } | |
| 121 | + return true | |
| 122 | + }) | |
| 112 | 123 | } |
| 113 | 124 | |
| 114 | 125 | // tools |
| 115 | − var tools []client.ToolDeclaration | |
| 116 | − toolsResult := gjson.GetBytes(rawJSON, "tools") | |
| 117 | − if toolsResult.IsArray() { | |
| 118 | − tools = make([]client.ToolDeclaration, 1) | |
| 119 | − tools[0].FunctionDeclarations = make([]any, 0) | |
| 120 | − toolsResults := toolsResult.Array() | |
| 121 | − for i := 0; i < len(toolsResults); i++ { | |
| 122 | − toolResult := toolsResults[i] | |
| 126 | + if toolsResult := gjson.GetBytes(rawJSON, "tools"); toolsResult.IsArray() { | |
| 127 | + hasTools := false | |
| 128 | + toolsResult.ForEach(func(_, toolResult gjson.Result) bool { | |
| 123 | 129 | inputSchemaResult := toolResult.Get("input_schema") |
| 124 | 130 | if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { |
| 125 | 131 | inputSchema := inputSchemaResult.Raw |
| @@ -129,30 +135,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 3 unmodified lines | ||
| 129 | 135 | tool, _ = sjson.Delete(tool, "input_examples") |
| 130 | 136 | tool, _ = sjson.Delete(tool, "type") |
| 131 | 137 | tool, _ = sjson.Delete(tool, "cache_control") |
| 132 | − var toolDeclaration any | |
| 133 | − if err := json.Unmarshal([]byte(tool), &toolDeclaration); err == nil { | |
| 134 | − tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, toolDeclaration) | |
| 138 | + if gjson.Valid(tool) && gjson.Parse(tool).IsObject() { | |
| 139 | + if !hasTools { | |
| 140 | + out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`) | |
| 141 | + hasTools = true | |
| 142 | + } | |
| 143 | + out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool) | |
| 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 | 153 | // Map Anthropic thinking -> Gemini thinkingBudget/include_thoughts when enabled |
| @@ -9,7 +9,6 @@ package claude · 8 unmodified lines | ||
| 9 | 9 | import ( |
| 10 | 10 | "bytes" |
| 11 | 11 | "context" |
| 12 | − "encoding/json" | |
| 13 | 12 | "fmt" |
| 14 | 13 | "strings" |
| 15 | 14 | "sync/atomic" |
| @@ -282,22 +281,16 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 266 unmodified lines | ||
| 282 | 281 | |
| 283 | 282 | root := gjson.ParseBytes(rawJSON) |
| 284 | 283 | |
| 285 | − response := map[string]interface{}{ | |
| 286 | − "id": root.Get("responseId").String(), | |
| 287 | − "type": "message", | |
| 288 | − "role": "assistant", | |
| 289 | − "model": root.Get("modelVersion").String(), | |
| 290 | − "content": []interface{}{}, | |
| 291 | − "stop_reason": nil, | |
| 292 | − "stop_sequence": nil, | |
| 293 | − "usage": map[string]interface{}{ | |
| 294 | − "input_tokens": root.Get("usageMetadata.promptTokenCount").Int(), | |
| 295 | − "output_tokens": root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int(), | |
| 296 | − }, | |
| 297 | − } | |
| 284 | + out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` | |
| 285 | + out, _ = sjson.Set(out, "id", root.Get("responseId").String()) | |
| 286 | + out, _ = sjson.Set(out, "model", root.Get("modelVersion").String()) | |
| 287 | + | |
| 288 | + inputTokens := root.Get("usageMetadata.promptTokenCount").Int() | |
| 289 | + outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int() | |
| 290 | + out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) | |
| 291 | + out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) | |
| 298 | 292 | |
| 299 | 293 | parts := root.Get("candidates.0.content.parts") |
| 300 | − var contentBlocks []interface{} | |
| 301 | 294 | textBuilder := strings.Builder{} |
| 302 | 295 | thinkingBuilder := strings.Builder{} |
| 303 | 296 | toolIDCounter := 0 |
| @@ -307,10 +300,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 3 unmodified lines | ||
| 307 | 300 | if textBuilder.Len() == 0 { |
| 308 | 301 | return |
| 309 | 302 | } |
| 310 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 311 | − "type": "text", | |
| 312 | − "text": textBuilder.String(), | |
| 313 | − }) | |
| 303 | + block := `{"type":"text","text":""}` | |
| 304 | + block, _ = sjson.Set(block, "text", textBuilder.String()) | |
| 305 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 314 | 306 | textBuilder.Reset() |
| 315 | 307 | } |
| 316 | 308 | |
| @@ -318,10 +310,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 1 unmodified line | ||
| 318 | 310 | if thinkingBuilder.Len() == 0 { |
| 319 | 311 | return |
| 320 | 312 | } |
| 321 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 322 | − "type": "thinking", | |
| 323 | − "thinking": thinkingBuilder.String(), | |
| 324 | − }) | |
| 313 | + block := `{"type":"thinking","thinking":""}` | |
| 314 | + block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) | |
| 315 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 325 | 316 | thinkingBuilder.Reset() |
| 326 | 317 | } |
| 327 | 318 | |
| @@ -345,21 +336,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 17 unmodified lines | ||
| 345 | 336 | |
| 346 | 337 | name := functionCall.Get("name").String() |
| 347 | 338 | toolIDCounter++ |
| 348 | − toolBlock := map[string]interface{}{ | |
| 349 | − "type": "tool_use", | |
| 350 | − "id": fmt.Sprintf("tool_%d", toolIDCounter), | |
| 351 | − "name": name, | |
| 352 | − "input": map[string]interface{}{}, | |
| 353 | − } | |
| 354 | − | |
| 355 | − if args := functionCall.Get("args"); args.Exists() { | |
| 356 | − var parsed interface{} | |
| 357 | − if err := json.Unmarshal([]byte(args.Raw), &parsed); err == nil { | |
| 358 | − toolBlock["input"] = parsed | |
| 359 | − } | |
| 339 | + toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 340 | + toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) | |
| 341 | + toolBlock, _ = sjson.Set(toolBlock, "name", name) | |
| 342 | + inputRaw := "{}" | |
| 343 | + if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() { | |
| 344 | + inputRaw = args.Raw | |
| 360 | 345 | } |
| 361 | − | |
| 362 | − contentBlocks = append(contentBlocks, toolBlock) | |
| 346 | + toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) | |
| 347 | + out, _ = sjson.SetRaw(out, "content.-1", toolBlock) | |
| 363 | 348 | continue |
| 364 | 349 | } |
| 365 | 350 | } |
| @@ -368,8 +353,6 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina · 2 unmodified lines | ||
| 368 | 353 | flushThinking() |
| 369 | 354 | flushText() |
| 370 | 355 | |
| 371 | − response["content"] = contentBlocks | |
| 372 | − | |
| 373 | 356 | stopReason := "end_turn" |
| 374 | 357 | if hasToolCall { |
| 375 | 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"] = stopReason | |
| 371 | + 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) { | |
| 391 | − if usageMeta := root.Get("usageMetadata"); !usageMeta.Exists() { | |
| 392 | − delete(response, "usage") | |
| 393 | − } | |
| 373 | + if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() { | |
| 374 | + out, _ = sjson.Delete(out, "usage") | |
| 394 | 375 | } |
| 395 | 376 | |
| 396 | − encoded, err := json.Marshal(response) | |
| 397 | − if err != nil { | |
| 398 | − return "" | |
| 399 | − } | |
| 400 | − return string(encoded) | |
| 377 | + return out | |
| 401 | 378 | } |
| 402 | 379 | |
| 403 | 380 | func ClaudeTokenCount(ctx context.Context, count int64) string { |
| @@ -320,7 +320,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 319 unmodified lines | ||
| 320 | 320 | log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) |
| 321 | 321 | continue |
| 322 | 322 | } |
| 323 | − fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{}) | |
| 323 | + fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) | |
| 324 | 324 | if errSet != nil { |
| 325 | 325 | log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) |
| 326 | 326 | continue |
| @@ -335,7 +335,7 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) · 8 unmodified lines | ||
| 335 | 335 | log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) |
| 336 | 336 | continue |
| 337 | 337 | } |
| 338 | − fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.properties", map[string]interface{}{}) | |
| 338 | + fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) | |
| 339 | 339 | if errSet != nil { |
| 340 | 340 | log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) |
| 341 | 341 | continue |
| @@ -8,7 +8,6 @@ package chat_completions · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "context" |
| 11 | − "encoding/json" | |
| 12 | 11 | "fmt" |
| 13 | 12 | "strings" |
| 14 | 13 | "sync/atomic" |
| @@ -173,21 +172,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR · 158 unmodified lines | ||
| 173 | 172 | mimeType = "image/png" |
| 174 | 173 | } |
| 175 | 174 | imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) |
| 176 | − imagePayload, err := json.Marshal(map[string]any{ | |
| 177 | − "type": "image_url", | |
| 178 | − "image_url": map[string]string{ | |
| 179 | − "url": imageURL, | |
| 180 | − }, | |
| 181 | − }) | |
| 182 | − if err != nil { | |
| 183 | − continue | |
| 184 | − } | |
| 175 | + imagePayload := `{"image_url":{"url":""},"type":"image_url"}` | |
| 176 | + imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) | |
| 185 | 177 | imagesResult := gjson.Get(template, "choices.0.delta.images") |
| 186 | 178 | if !imagesResult.Exists() || !imagesResult.IsArray() { |
| 187 | 179 | template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`) |
| 188 | 180 | } |
| 189 | 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 | 297 | mimeType = "image/png" |
| 306 | 298 | } |
| 307 | 299 | imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) |
| 308 | − imagePayload, err := json.Marshal(map[string]any{ | |
| 309 | − "type": "image_url", | |
| 310 | − "image_url": map[string]string{ | |
| 311 | − "url": imageURL, | |
| 312 | − }, | |
| 313 | − }) | |
| 314 | − if err != nil { | |
| 315 | − continue | |
| 316 | − } | |
| 300 | + imagePayload := `{"image_url":{"url":""},"type":"image_url"}` | |
| 301 | + imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) | |
| 317 | 302 | imagesResult := gjson.Get(template, "choices.0.message.images") |
| 318 | 303 | if !imagesResult.Exists() || !imagesResult.IsArray() { |
| 319 | 304 | template, _ = sjson.SetRaw(template, "choices.0.message.images", `[]`) |
| 320 | 305 | } |
| 321 | 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 | } |
| @@ -377,27 +377,18 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, · 376 unmodified lines | ||
| 377 | 377 | } |
| 378 | 378 | |
| 379 | 379 | // Compose outputs in encountered order: reasoning, message, function_calls |
| 380 | − var outputs []interface{} | |
| 380 | + outputsWrapper := `{"arr":[]}` | |
| 381 | 381 | if st.ReasoningOpened { |
| 382 | − outputs = append(outputs, map[string]interface{}{ | |
| 383 | − "id": st.ReasoningItemID, | |
| 384 | − "type": "reasoning", | |
| 385 | − "summary": []interface{}{map[string]interface{}{"type": "summary_text", "text": st.ReasoningBuf.String()}}, | |
| 386 | − }) | |
| 382 | + item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` | |
| 383 | + item, _ = sjson.Set(item, "id", st.ReasoningItemID) | |
| 384 | + item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String()) | |
| 385 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 387 | 386 | } |
| 388 | 387 | if st.MsgOpened { |
| 389 | − outputs = append(outputs, map[string]interface{}{ | |
| 390 | − "id": st.CurrentMsgID, | |
| 391 | − "type": "message", | |
| 392 | − "status": "completed", | |
| 393 | − "content": []interface{}{map[string]interface{}{ | |
| 394 | − "type": "output_text", | |
| 395 | − "annotations": []interface{}{}, | |
| 396 | − "logprobs": []interface{}{}, | |
| 397 | − "text": st.TextBuf.String(), | |
| 398 | − }}, | |
| 399 | − "role": "assistant", | |
| 400 | − }) | |
| 388 | + item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` | |
| 389 | + item, _ = sjson.Set(item, "id", st.CurrentMsgID) | |
| 390 | + item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String()) | |
| 391 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 401 | 392 | } |
| 402 | 393 | if len(st.FuncArgsBuf) > 0 { |
| 403 | 394 | idxs := make([]int, 0, len(st.FuncArgsBuf)) |
| @@ -416,18 +407,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, · 12 unmodified lines | ||
| 416 | 407 | if b := st.FuncArgsBuf[idx]; b != nil { |
| 417 | 408 | args = b.String() |
| 418 | 409 | } |
| 419 | − outputs = append(outputs, map[string]interface{}{ | |
| 420 | − "id": fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]), | |
| 421 | − "type": "function_call", | |
| 422 | − "status": "completed", | |
| 423 | − "arguments": args, | |
| 424 | − "call_id": st.FuncCallIDs[idx], | |
| 425 | − "name": st.FuncNames[idx], | |
| 426 | − }) | |
| 410 | + item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` | |
| 411 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) | |
| 412 | + item, _ = sjson.Set(item, "arguments", args) | |
| 413 | + item, _ = sjson.Set(item, "call_id", st.FuncCallIDs[idx]) | |
| 414 | + item, _ = sjson.Set(item, "name", st.FuncNames[idx]) | |
| 415 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 427 | 416 | } |
| 428 | 417 | } |
| 429 | − if len(outputs) > 0 { | |
| 430 | − completed, _ = sjson.Set(completed, "response.output", outputs) | |
| 418 | + if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { | |
| 419 | + completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw) | |
| 431 | 420 | } |
| 432 | 421 | |
| 433 | 422 | // usage mapping |
| @@ -558,11 +547,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 124 unmodified lines | ||
| 558 | 547 | } |
| 559 | 548 | |
| 560 | 549 | // Build outputs from candidates[0].content.parts |
| 561 | − var outputs []interface{} | |
| 562 | 550 | var reasoningText strings.Builder |
| 563 | 551 | var reasoningEncrypted string |
| 564 | 552 | var messageText strings.Builder |
| 565 | 553 | 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 | 568 | if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() { |
| 567 | 569 | parts.ForEach(func(_, p gjson.Result) bool { |
| 568 | 570 | if p.Get("thought").Bool() { |
| @@ -583,19 +585,16 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 14 unmodified lines | ||
| 583 | 585 | name := fc.Get("name").String() |
| 584 | 586 | args := fc.Get("args") |
| 585 | 587 | callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1)) |
| 586 | − outputs = append(outputs, map[string]interface{}{ | |
| 587 | − "id": fmt.Sprintf("fc_%s", callID), | |
| 588 | − "type": "function_call", | |
| 589 | − "status": "completed", | |
| 590 | − "arguments": func() string { | |
| 591 | − if args.Exists() { | |
| 592 | − return args.Raw | |
| 593 | − } | |
| 594 | − return "" | |
| 595 | − }(), | |
| 596 | − "call_id": callID, | |
| 597 | − "name": name, | |
| 598 | − }) | |
| 588 | + itemJSON := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` | |
| 589 | + itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID)) | |
| 590 | + itemJSON, _ = sjson.Set(itemJSON, "call_id", callID) | |
| 591 | + itemJSON, _ = sjson.Set(itemJSON, "name", name) | |
| 592 | + argsStr := "" | |
| 593 | + if args.Exists() { | |
| 594 | + argsStr = args.Raw | |
| 595 | + } | |
| 596 | + itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr) | |
| 597 | + appendOutput(itemJSON) | |
| 599 | 598 | return true |
| 600 | 599 | } |
| 601 | 600 | return true |
| @@ -605,42 +604,24 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string · 3 unmodified lines | ||
| 605 | 604 | // Reasoning output item |
| 606 | 605 | if reasoningText.Len() > 0 || reasoningEncrypted != "" { |
| 607 | 606 | rid := strings.TrimPrefix(id, "resp_") |
| 608 | − item := map[string]interface{}{ | |
| 609 | − "id": fmt.Sprintf("rs_%s", rid), | |
| 610 | − "type": "reasoning", | |
| 611 | − "encrypted_content": reasoningEncrypted, | |
| 612 | − } | |
| 613 | − var summaries []interface{} | |
| 607 | + itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}` | |
| 608 | + itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid)) | |
| 609 | + itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted) | |
| 614 | 610 | if reasoningText.Len() > 0 { |
| 615 | − summaries = append(summaries, map[string]interface{}{ | |
| 616 | − "type": "summary_text", | |
| 617 | − "text": reasoningText.String(), | |
| 618 | − }) | |
| 619 | − } | |
| 620 | − if summaries != nil { | |
| 621 | − item["summary"] = summaries | |
| 611 | + summaryJSON := `{"type":"summary_text","text":""}` | |
| 612 | + summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String()) | |
| 613 | + itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]") | |
| 614 | + itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON) | |
| 622 | 615 | } |
| 623 | − outputs = append(outputs, item) | |
| 616 | + appendOutput(itemJSON) | |
| 624 | 617 | } |
| 625 | 618 | |
| 626 | 619 | // Assistant message output item |
| 627 | 620 | if haveMessage { |
| 628 | − outputs = append(outputs, map[string]interface{}{ | |
| 629 | − "id": fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")), | |
| 630 | − "type": "message", | |
| 631 | − "status": "completed", | |
| 632 | − "content": []interface{}{map[string]interface{}{ | |
| 633 | − "type": "output_text", | |
| 634 | − "annotations": []interface{}{}, | |
| 635 | − "logprobs": []interface{}{}, | |
| 636 | − "text": messageText.String(), | |
| 637 | − }}, | |
| 638 | − "role": "assistant", | |
| 639 | − }) | |
| 640 | − } | |
| 641 | − | |
| 642 | − if len(outputs) > 0 { | |
| 643 | − resp, _ = sjson.Set(resp, "output", outputs) | |
| 621 | + itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` | |
| 622 | + itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_"))) | |
| 623 | + itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String()) | |
| 624 | + appendOutput(itemJSON) | |
| 644 | 625 | } |
| 645 | 626 | |
| 646 | 627 | // usage mapping |
| @@ -7,7 +7,6 @@ package claude · 6 unmodified lines | ||
| 7 | 7 | |
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | − "encoding/json" | |
| 11 | 10 | "strings" |
| 12 | 11 | |
| 13 | 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 | 138 | // Convert input to arguments JSON string |
| 140 | 139 | if input := part.Get("input"); input.Exists() { |
| 141 | − if inputJSON, err := json.Marshal(input.Value()); err == nil { | |
| 142 | − toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", string(inputJSON)) | |
| 143 | − } else { | |
| 144 | − toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}") | |
| 145 | − } | |
| 140 | + toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw) | |
| 146 | 141 | } else { |
| 147 | 142 | toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}") |
| 148 | 143 | } |
| @@ -191,8 +186,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 42 unmodified lines | ||
| 191 | 186 | // Emit tool calls in a separate assistant message |
| 192 | 187 | if role == "assistant" && len(toolCalls) > 0 { |
| 193 | 188 | toolCallMsgJSON := `{"role":"assistant","tool_calls":[]}` |
| 194 | − toolCallsJSON, _ := json.Marshal(toolCalls) | |
| 195 | − toolCallMsgJSON, _ = sjson.SetRaw(toolCallMsgJSON, "tool_calls", string(toolCallsJSON)) | |
| 189 | + toolCallMsgJSON, _ = sjson.Set(toolCallMsgJSON, "tool_calls", toolCalls) | |
| 196 | 190 | messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolCallMsgJSON).Value()) |
| 197 | 191 | } |
| 198 | 192 | |
| @@ -8,7 +8,6 @@ package claude · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "context" |
| 11 | − "encoding/json" | |
| 12 | 11 | "fmt" |
| 13 | 12 | "strings" |
| 14 | 13 | |
| @@ -133,24 +132,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 118 unmodified lines | ||
| 133 | 132 | if delta := root.Get("choices.0.delta"); delta.Exists() { |
| 134 | 133 | if !param.MessageStarted { |
| 135 | 134 | // Send message_start event |
| 136 | − messageStart := map[string]interface{}{ | |
| 137 | − "type": "message_start", | |
| 138 | − "message": map[string]interface{}{ | |
| 139 | − "id": param.MessageID, | |
| 140 | − "type": "message", | |
| 141 | − "role": "assistant", | |
| 142 | − "model": param.Model, | |
| 143 | − "content": []interface{}{}, | |
| 144 | − "stop_reason": nil, | |
| 145 | − "stop_sequence": nil, | |
| 146 | − "usage": map[string]interface{}{ | |
| 147 | − "input_tokens": 0, | |
| 148 | − "output_tokens": 0, | |
| 149 | − }, | |
| 150 | − }, | |
| 151 | − } | |
| 152 | − messageStartJSON, _ := json.Marshal(messageStart) | |
| 153 | − results = append(results, "event: message_start\ndata: "+string(messageStartJSON)+"\n\n") | |
| 135 | + messageStartJSON := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}` | |
| 136 | + messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID) | |
| 137 | + messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model) | |
| 138 | + results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n") | |
| 154 | 139 | param.MessageStarted = true |
| 155 | 140 | |
| 156 | 141 | // 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 | 153 | param.ThinkingContentBlockIndex = param.NextContentBlockIndex |
| 169 | 154 | param.NextContentBlockIndex++ |
| 170 | 155 | } |
| 171 | − contentBlockStart := map[string]interface{}{ | |
| 172 | − "type": "content_block_start", | |
| 173 | − "index": param.ThinkingContentBlockIndex, | |
| 174 | − "content_block": map[string]interface{}{ | |
| 175 | − "type": "thinking", | |
| 176 | − "thinking": "", | |
| 177 | − }, | |
| 178 | − } | |
| 179 | − contentBlockStartJSON, _ := json.Marshal(contentBlockStart) | |
| 180 | − results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n") | |
| 156 | + contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}` | |
| 157 | + contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex) | |
| 158 | + results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n") | |
| 181 | 159 | param.ThinkingContentBlockStarted = true |
| 182 | 160 | } |
| 183 | 161 | |
| 184 | − thinkingDelta := map[string]interface{}{ | |
| 185 | − "type": "content_block_delta", | |
| 186 | − "index": param.ThinkingContentBlockIndex, | |
| 187 | − "delta": map[string]interface{}{ | |
| 188 | − "type": "thinking_delta", | |
| 189 | − "thinking": reasoningText, | |
| 190 | − }, | |
| 191 | − } | |
| 192 | − thinkingDeltaJSON, _ := json.Marshal(thinkingDelta) | |
| 193 | − results = append(results, "event: content_block_delta\ndata: "+string(thinkingDeltaJSON)+"\n\n") | |
| 162 | + thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}` | |
| 163 | + thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex) | |
| 164 | + thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText) | |
| 165 | + results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n") | |
| 194 | 166 | } |
| 195 | 167 | } |
| 196 | 168 | |
| @@ -203,29 +175,16 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 6 unmodified lines | ||
| 203 | 175 | param.TextContentBlockIndex = param.NextContentBlockIndex |
| 204 | 176 | param.NextContentBlockIndex++ |
| 205 | 177 | } |
| 206 | − contentBlockStart := map[string]interface{}{ | |
| 207 | − "type": "content_block_start", | |
| 208 | − "index": param.TextContentBlockIndex, | |
| 209 | − "content_block": map[string]interface{}{ | |
| 210 | − "type": "text", | |
| 211 | − "text": "", | |
| 212 | − }, | |
| 213 | − } | |
| 214 | − contentBlockStartJSON, _ := json.Marshal(contentBlockStart) | |
| 215 | − results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n") | |
| 178 | + contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}` | |
| 179 | + contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex) | |
| 180 | + results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n") | |
| 216 | 181 | param.TextContentBlockStarted = true |
| 217 | 182 | } |
| 218 | 183 | |
| 219 | − contentDelta := map[string]interface{}{ | |
| 220 | − "type": "content_block_delta", | |
| 221 | − "index": param.TextContentBlockIndex, | |
| 222 | − "delta": map[string]interface{}{ | |
| 223 | − "type": "text_delta", | |
| 224 | − "text": content.String(), | |
| 225 | − }, | |
| 226 | − } | |
| 227 | − contentDeltaJSON, _ := json.Marshal(contentDelta) | |
| 228 | − results = append(results, "event: content_block_delta\ndata: "+string(contentDeltaJSON)+"\n\n") | |
| 184 | + contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}` | |
| 185 | + contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex) | |
| 186 | + contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String()) | |
| 187 | + results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n") | |
| 229 | 188 | |
| 230 | 189 | // Accumulate content |
| 231 | 190 | param.ContentAccumulator.WriteString(content.String()) |
| @@ -263,18 +222,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 31 unmodified lines | ||
| 263 | 222 | stopTextContentBlock(param, &results) |
| 264 | 223 | |
| 265 | 224 | // Send content_block_start for tool_use |
| 266 | − contentBlockStart := map[string]interface{}{ | |
| 267 | − "type": "content_block_start", | |
| 268 | − "index": blockIndex, | |
| 269 | − "content_block": map[string]interface{}{ | |
| 270 | − "type": "tool_use", | |
| 271 | − "id": accumulator.ID, | |
| 272 | − "name": accumulator.Name, | |
| 273 | − "input": map[string]interface{}{}, | |
| 274 | − }, | |
| 275 | − } | |
| 276 | − contentBlockStartJSON, _ := json.Marshal(contentBlockStart) | |
| 277 | − results = append(results, "event: content_block_start\ndata: "+string(contentBlockStartJSON)+"\n\n") | |
| 225 | + contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}` | |
| 226 | + contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex) | |
| 227 | + contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", accumulator.ID) | |
| 228 | + contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name) | |
| 229 | + results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n") | |
| 278 | 230 | } |
| 279 | 231 | |
| 280 | 232 | // Handle function arguments |
| @@ -298,12 +250,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 17 unmodified lines | ||
| 298 | 250 | |
| 299 | 251 | // Send content_block_stop for thinking content if needed |
| 300 | 252 | if param.ThinkingContentBlockStarted { |
| 301 | − contentBlockStop := map[string]interface{}{ | |
| 302 | − "type": "content_block_stop", | |
| 303 | − "index": param.ThinkingContentBlockIndex, | |
| 304 | − } | |
| 305 | − contentBlockStopJSON, _ := json.Marshal(contentBlockStop) | |
| 306 | − results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n") | |
| 253 | + contentBlockStopJSON := `{"type":"content_block_stop","index":0}` | |
| 254 | + contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) | |
| 255 | + results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") | |
| 307 | 256 | param.ThinkingContentBlockStarted = false |
| 308 | 257 | param.ThinkingContentBlockIndex = -1 |
| 309 | 258 | } |
| @@ -319,24 +268,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 9 unmodified lines | ||
| 319 | 268 | |
| 320 | 269 | // Send complete input_json_delta with all accumulated arguments |
| 321 | 270 | if accumulator.Arguments.Len() > 0 { |
| 322 | − inputDelta := map[string]interface{}{ | |
| 323 | − "type": "content_block_delta", | |
| 324 | − "index": blockIndex, | |
| 325 | − "delta": map[string]interface{}{ | |
| 326 | − "type": "input_json_delta", | |
| 327 | − "partial_json": util.FixJSON(accumulator.Arguments.String()), | |
| 328 | − }, | |
| 329 | − } | |
| 330 | − inputDeltaJSON, _ := json.Marshal(inputDelta) | |
| 331 | − results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n") | |
| 271 | + inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` | |
| 272 | + inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex) | |
| 273 | + inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String())) | |
| 274 | + results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n") | |
| 332 | 275 | } |
| 333 | 276 | |
| 334 | − contentBlockStop := map[string]interface{}{ | |
| 335 | − "type": "content_block_stop", | |
| 336 | − "index": blockIndex, | |
| 337 | − } | |
| 338 | − contentBlockStopJSON, _ := json.Marshal(contentBlockStop) | |
| 339 | − results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n") | |
| 277 | + contentBlockStopJSON := `{"type":"content_block_stop","index":0}` | |
| 278 | + contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex) | |
| 279 | + results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") | |
| 340 | 280 | delete(param.ToolCallBlockIndexes, index) |
| 341 | 281 | } |
| 342 | 282 | param.ContentBlocksStopped = true |
| @@ -361,20 +301,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI · 18 unmodified lines | ||
| 361 | 301 | } |
| 362 | 302 | } |
| 363 | 303 | // Send message_delta with usage |
| 364 | − messageDelta := map[string]interface{}{ | |
| 365 | − "type": "message_delta", | |
| 366 | − "delta": map[string]interface{}{ | |
| 367 | − "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason), | |
| 368 | − "stop_sequence": nil, | |
| 369 | − }, | |
| 370 | − "usage": map[string]interface{}{ | |
| 371 | − "input_tokens": inputTokens, | |
| 372 | − "output_tokens": outputTokens, | |
| 373 | − }, | |
| 374 | − } | |
| 375 | − | |
| 376 | − messageDeltaJSON, _ := json.Marshal(messageDelta) | |
| 377 | − results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n") | |
| 304 | + messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` | |
| 305 | + messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason)) | |
| 306 | + messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens) | |
| 307 | + messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens) | |
| 308 | + results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n") | |
| 378 | 309 | param.MessageDeltaSent = true |
| 379 | 310 | |
| 380 | 311 | emitMessageStopIfNeeded(param, &results) |
| @@ -390,12 +321,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 9 unmodified lines | ||
| 390 | 321 | |
| 391 | 322 | // Ensure all content blocks are stopped before final events |
| 392 | 323 | if param.ThinkingContentBlockStarted { |
| 393 | − contentBlockStop := map[string]interface{}{ | |
| 394 | − "type": "content_block_stop", | |
| 395 | − "index": param.ThinkingContentBlockIndex, | |
| 396 | − } | |
| 397 | − contentBlockStopJSON, _ := json.Marshal(contentBlockStop) | |
| 398 | − results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n") | |
| 324 | + contentBlockStopJSON := `{"type":"content_block_stop","index":0}` | |
| 325 | + contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) | |
| 326 | + results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") | |
| 399 | 327 | param.ThinkingContentBlockStarted = false |
| 400 | 328 | param.ThinkingContentBlockIndex = -1 |
| 401 | 329 | } |
| @@ -408,24 +336,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 6 unmodified lines | ||
| 408 | 336 | blockIndex := param.toolContentBlockIndex(index) |
| 409 | 337 | |
| 410 | 338 | if accumulator.Arguments.Len() > 0 { |
| 411 | − inputDelta := map[string]interface{}{ | |
| 412 | − "type": "content_block_delta", | |
| 413 | − "index": blockIndex, | |
| 414 | − "delta": map[string]interface{}{ | |
| 415 | − "type": "input_json_delta", | |
| 416 | − "partial_json": util.FixJSON(accumulator.Arguments.String()), | |
| 417 | − }, | |
| 418 | − } | |
| 419 | − inputDeltaJSON, _ := json.Marshal(inputDelta) | |
| 420 | − results = append(results, "event: content_block_delta\ndata: "+string(inputDeltaJSON)+"\n\n") | |
| 339 | + inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` | |
| 340 | + inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex) | |
| 341 | + inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String())) | |
| 342 | + results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n") | |
| 421 | 343 | } |
| 422 | 344 | |
| 423 | − contentBlockStop := map[string]interface{}{ | |
| 424 | − "type": "content_block_stop", | |
| 425 | − "index": blockIndex, | |
| 426 | − } | |
| 427 | − contentBlockStopJSON, _ := json.Marshal(contentBlockStop) | |
| 428 | − results = append(results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n") | |
| 345 | + contentBlockStopJSON := `{"type":"content_block_stop","index":0}` | |
| 346 | + contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex) | |
| 347 | + results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") | |
| 429 | 348 | delete(param.ToolCallBlockIndexes, index) |
| 430 | 349 | } |
| 431 | 350 | param.ContentBlocksStopped = true |
| @@ -433,16 +352,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 1 unmodified line | ||
| 433 | 352 | |
| 434 | 353 | // If we haven't sent message_delta yet (no usage info was received), send it now |
| 435 | 354 | if param.FinishReason != "" && !param.MessageDeltaSent { |
| 436 | − messageDelta := map[string]interface{}{ | |
| 437 | − "type": "message_delta", | |
| 438 | − "delta": map[string]interface{}{ | |
| 439 | − "stop_reason": mapOpenAIFinishReasonToAnthropic(param.FinishReason), | |
| 440 | − "stop_sequence": nil, | |
| 441 | − }, | |
| 442 | − } | |
| 443 | − | |
| 444 | − messageDeltaJSON, _ := json.Marshal(messageDelta) | |
| 445 | − results = append(results, "event: message_delta\ndata: "+string(messageDeltaJSON)+"\n\n") | |
| 355 | + messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null}}` | |
| 356 | + messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(param.FinishReason)) | |
| 357 | + results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n") | |
| 446 | 358 | param.MessageDeltaSent = true |
| 447 | 359 | } |
| 448 | 360 | |
| @@ -455,105 +367,73 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) · 6 unmodified lines | ||
| 455 | 367 | func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string { |
| 456 | 368 | root := gjson.ParseBytes(rawJSON) |
| 457 | 369 | |
| 458 | − // Build Anthropic response | |
| 459 | − response := map[string]interface{}{ | |
| 460 | − "id": root.Get("id").String(), | |
| 461 | − "type": "message", | |
| 462 | − "role": "assistant", | |
| 463 | − "model": root.Get("model").String(), | |
| 464 | − "content": []interface{}{}, | |
| 465 | − "stop_reason": nil, | |
| 466 | − "stop_sequence": nil, | |
| 467 | − "usage": map[string]interface{}{ | |
| 468 | − "input_tokens": 0, | |
| 469 | − "output_tokens": 0, | |
| 470 | − }, | |
| 471 | − } | |
| 370 | + out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` | |
| 371 | + out, _ = sjson.Set(out, "id", root.Get("id").String()) | |
| 372 | + out, _ = sjson.Set(out, "model", root.Get("model").String()) | |
| 472 | 373 | |
| 473 | 374 | // Process message content and tool calls |
| 474 | − var contentBlocks []interface{} | |
| 475 | − | |
| 476 | − if choices := root.Get("choices"); choices.Exists() && choices.IsArray() { | |
| 375 | + if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 { | |
| 477 | 376 | 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 | 380 | if reasoningText == "" { |
| 483 | 381 | continue |
| 484 | 382 | } |
| 485 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 486 | − "type": "thinking", | |
| 487 | − "thinking": reasoningText, | |
| 488 | − }) | |
| 383 | + block := `{"type":"thinking","thinking":""}` | |
| 384 | + block, _ = sjson.Set(block, "thinking", reasoningText) | |
| 385 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 489 | 386 | } |
| 490 | 387 | |
| 491 | 388 | // Handle text content |
| 492 | 389 | if content := choice.Get("message.content"); content.Exists() && content.String() != "" { |
| 493 | − textBlock := map[string]interface{}{ | |
| 494 | − "type": "text", | |
| 495 | − "text": content.String(), | |
| 496 | − } | |
| 497 | − contentBlocks = append(contentBlocks, textBlock) | |
| 390 | + block := `{"type":"text","text":""}` | |
| 391 | + block, _ = sjson.Set(block, "text", content.String()) | |
| 392 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 498 | 393 | } |
| 499 | 394 | |
| 500 | 395 | // Handle tool calls |
| 501 | 396 | if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() { |
| 502 | 397 | toolCalls.ForEach(func(_, toolCall gjson.Result) bool { |
| 503 | − toolUseBlock := map[string]interface{}{ | |
| 504 | − "type": "tool_use", | |
| 505 | − "id": toolCall.Get("id").String(), | |
| 506 | − "name": toolCall.Get("function.name").String(), | |
| 507 | − } | |
| 508 | − | |
| 509 | − // Parse arguments | |
| 510 | − argsStr := toolCall.Get("function.arguments").String() | |
| 511 | − argsStr = util.FixJSON(argsStr) | |
| 512 | − if argsStr != "" { | |
| 513 | − var args interface{} | |
| 514 | − if err := json.Unmarshal([]byte(argsStr), &args); err == nil { | |
| 515 | − toolUseBlock["input"] = args | |
| 398 | + toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 399 | + toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String()) | |
| 400 | + toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String()) | |
| 401 | + | |
| 402 | + argsStr := util.FixJSON(toolCall.Get("function.arguments").String()) | |
| 403 | + if argsStr != "" && gjson.Valid(argsStr) { | |
| 404 | + argsJSON := gjson.Parse(argsStr) | |
| 405 | + if argsJSON.IsObject() { | |
| 406 | + toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw) | |
| 516 | 407 | } else { |
| 517 | − toolUseBlock["input"] = map[string]interface{}{} | |
| 408 | + toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}") | |
| 518 | 409 | } |
| 519 | 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 | 415 | return true |
| 525 | 416 | }) |
| 526 | 417 | } |
| 527 | 418 | |
| 528 | 419 | // Set stop reason |
| 529 | 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 | 425 | // Set usage information |
| 537 | 426 | if usage := root.Get("usage"); usage.Exists() { |
| 538 | − response["usage"] = map[string]interface{}{ | |
| 539 | − "input_tokens": usage.Get("prompt_tokens").Int(), | |
| 540 | − "output_tokens": usage.Get("completion_tokens").Int(), | |
| 541 | − "reasoning_tokens": func() int64 { | |
| 542 | − if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() { | |
| 543 | − return v.Int() | |
| 544 | − } | |
| 545 | − return 0 | |
| 546 | − }(), | |
| 547 | − } | |
| 548 | − } else { | |
| 549 | − response["usage"] = map[string]interface{}{ | |
| 550 | − "input_tokens": 0, | |
| 551 | − "output_tokens": 0, | |
| 427 | + out, _ = sjson.Set(out, "usage.input_tokens", usage.Get("prompt_tokens").Int()) | |
| 428 | + out, _ = sjson.Set(out, "usage.output_tokens", usage.Get("completion_tokens").Int()) | |
| 429 | + reasoningTokens := int64(0) | |
| 430 | + if v := usage.Get("completion_tokens_details.reasoning_tokens"); v.Exists() { | |
| 431 | + reasoningTokens = v.Int() | |
| 552 | 432 | } |
| 433 | + out, _ = sjson.Set(out, "usage.reasoning_tokens", reasoningTokens) | |
| 553 | 434 | } |
| 554 | 435 | |
| 555 | − responseJSON, _ := json.Marshal(response) | |
| 556 | − return []string{string(responseJSON)} | |
| 436 | + return []string{out} | |
| 557 | 437 | } |
| 558 | 438 | |
| 559 | 439 | // mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents |
| @@ -620,12 +500,9 @@ func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, res · 60 unmodified lines | ||
| 620 | 500 | if !param.ThinkingContentBlockStarted { |
| 621 | 501 | return |
| 622 | 502 | } |
| 623 | − contentBlockStop := map[string]interface{}{ | |
| 624 | − "type": "content_block_stop", | |
| 625 | − "index": param.ThinkingContentBlockIndex, | |
| 626 | − } | |
| 627 | − contentBlockStopJSON, _ := json.Marshal(contentBlockStop) | |
| 628 | − *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n") | |
| 503 | + contentBlockStopJSON := `{"type":"content_block_stop","index":0}` | |
| 504 | + contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) | |
| 505 | + *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") | |
| 629 | 506 | param.ThinkingContentBlockStarted = false |
| 630 | 507 | param.ThinkingContentBlockIndex = -1 |
| 631 | 508 | } |
| @@ -642,12 +519,9 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results · 10 unmodified lines | ||
| 642 | 519 | if !param.TextContentBlockStarted { |
| 643 | 520 | return |
| 644 | 521 | } |
| 645 | − contentBlockStop := map[string]interface{}{ | |
| 646 | − "type": "content_block_stop", | |
| 647 | − "index": param.TextContentBlockIndex, | |
| 648 | − } | |
| 649 | − contentBlockStopJSON, _ := json.Marshal(contentBlockStop) | |
| 650 | − *results = append(*results, "event: content_block_stop\ndata: "+string(contentBlockStopJSON)+"\n\n") | |
| 522 | + contentBlockStopJSON := `{"type":"content_block_stop","index":0}` | |
| 523 | + contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex) | |
| 524 | + *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") | |
| 651 | 525 | param.TextContentBlockStarted = false |
| 652 | 526 | param.TextContentBlockIndex = -1 |
| 653 | 527 | } |
| @@ -667,29 +541,19 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 13 unmodified lines | ||
| 667 | 541 | _ = requestRawJSON |
| 668 | 542 | |
| 669 | 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 | 548 | hasToolCall := false |
| 549 | + stopReasonSet := false | |
| 687 | 550 | |
| 688 | 551 | if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 { |
| 689 | 552 | choice := choices.Array()[0] |
| 690 | 553 | |
| 691 | 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 | 559 | if message := choice.Get("message"); message.Exists() { |
| @@ -702,10 +566,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 6 unmodified lines | ||
| 702 | 566 | if textBuilder.Len() == 0 { |
| 703 | 567 | return |
| 704 | 568 | } |
| 705 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 706 | − "type": "text", | |
| 707 | − "text": textBuilder.String(), | |
| 708 | − }) | |
| 569 | + block := `{"type":"text","text":""}` | |
| 570 | + block, _ = sjson.Set(block, "text", textBuilder.String()) | |
| 571 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 709 | 572 | textBuilder.Reset() |
| 710 | 573 | } |
| 711 | 574 | |
| @@ -713,16 +576,14 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 1 unmodified line | ||
| 713 | 576 | if thinkingBuilder.Len() == 0 { |
| 714 | 577 | return |
| 715 | 578 | } |
| 716 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 717 | − "type": "thinking", | |
| 718 | − "thinking": thinkingBuilder.String(), | |
| 719 | − }) | |
| 579 | + block := `{"type":"thinking","thinking":""}` | |
| 580 | + block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) | |
| 581 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 720 | 582 | thinkingBuilder.Reset() |
| 721 | 583 | } |
| 722 | 584 | |
| 723 | 585 | for _, item := range contentResult.Array() { |
| 724 | − typeStr := item.Get("type").String() | |
| 725 | − switch typeStr { | |
| 586 | + switch item.Get("type").String() { | |
| 726 | 587 | case "text": |
| 727 | 588 | flushThinking() |
| 728 | 589 | textBuilder.WriteString(item.Get("text").String()) |
| @@ -733,25 +594,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 4 unmodified lines | ||
| 733 | 594 | if toolCalls.IsArray() { |
| 734 | 595 | toolCalls.ForEach(func(_, tc gjson.Result) bool { |
| 735 | 596 | hasToolCall = true |
| 736 | − toolUse := map[string]interface{}{ | |
| 737 | − "type": "tool_use", | |
| 738 | − "id": tc.Get("id").String(), | |
| 739 | − "name": tc.Get("function.name").String(), | |
| 740 | − } | |
| 597 | + toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 598 | + toolUse, _ = sjson.Set(toolUse, "id", tc.Get("id").String()) | |
| 599 | + toolUse, _ = sjson.Set(toolUse, "name", tc.Get("function.name").String()) | |
| 741 | 600 | |
| 742 | 601 | argsStr := util.FixJSON(tc.Get("function.arguments").String()) |
| 743 | − if argsStr != "" { | |
| 744 | − var parsed interface{} | |
| 745 | − if err := json.Unmarshal([]byte(argsStr), &parsed); err == nil { | |
| 746 | − toolUse["input"] = parsed | |
| 602 | + if argsStr != "" && gjson.Valid(argsStr) { | |
| 603 | + argsJSON := gjson.Parse(argsStr) | |
| 604 | + if argsJSON.IsObject() { | |
| 605 | + toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw) | |
| 747 | 606 | } else { |
| 748 | − toolUse["input"] = map[string]interface{}{} | |
| 607 | + toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") | |
| 749 | 608 | } |
| 750 | 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 | 614 | return true |
| 756 | 615 | }) |
| 757 | 616 | } |
| @@ -771,10 +630,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 13 unmodified lines | ||
| 771 | 630 | } else if contentResult.Type == gjson.String { |
| 772 | 631 | textContent := contentResult.String() |
| 773 | 632 | if textContent != "" { |
| 774 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 775 | − "type": "text", | |
| 776 | − "text": textContent, | |
| 777 | − }) | |
| 633 | + block := `{"type":"text","text":""}` | |
| 634 | + block, _ = sjson.Set(block, "text", textContent) | |
| 635 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 778 | 636 | } |
| 779 | 637 | } |
| 780 | 638 | } |
| @@ -784,81 +642,52 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina · 3 unmodified lines | ||
| 784 | 642 | if reasoningText == "" { |
| 785 | 643 | continue |
| 786 | 644 | } |
| 787 | − contentBlocks = append(contentBlocks, map[string]interface{}{ | |
| 788 | − "type": "thinking", | |
| 789 | − "thinking": reasoningText, | |
| 790 | − }) | |
| 645 | + block := `{"type":"thinking","thinking":""}` | |
| 646 | + block, _ = sjson.Set(block, "thinking", reasoningText) | |
| 647 | + out, _ = sjson.SetRaw(out, "content.-1", block) | |
| 791 | 648 | } |
| 792 | 649 | } |
| 793 | 650 | |
| 794 | 651 | if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() { |
| 795 | 652 | toolCalls.ForEach(func(_, toolCall gjson.Result) bool { |
| 796 | 653 | hasToolCall = true |
| 797 | − toolUseBlock := map[string]interface{}{ | |
| 798 | − "type": "tool_use", | |
| 799 | − "id": toolCall.Get("id").String(), | |
| 800 | − "name": toolCall.Get("function.name").String(), | |
| 801 | − } | |
| 802 | − | |
| 803 | − argsStr := toolCall.Get("function.arguments").String() | |
| 804 | − argsStr = util.FixJSON(argsStr) | |
| 805 | − if argsStr != "" { | |
| 806 | − var args interface{} | |
| 807 | − if err := json.Unmarshal([]byte(argsStr), &args); err == nil { | |
| 808 | − toolUseBlock["input"] = args | |
| 654 | + toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}` | |
| 655 | + toolUseBlock, _ = sjson.Set(toolUseBlock, "id", toolCall.Get("id").String()) | |
| 656 | + toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String()) | |
| 657 | + | |
| 658 | + argsStr := util.FixJSON(toolCall.Get("function.arguments").String()) | |
| 659 | + if argsStr != "" && gjson.Valid(argsStr) { | |
| 660 | + argsJSON := gjson.Parse(argsStr) | |
| 661 | + if argsJSON.IsObject() { | |
| 662 | + toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw) | |
| 809 | 663 | } else { |
| 810 | − toolUseBlock["input"] = map[string]interface{}{} | |
| 664 | + toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}") | |
| 811 | 665 | } |
| 812 | 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 | 671 | return true |
| 818 | 672 | }) |
| 819 | 673 | } |
| 820 | 674 | } |
| 821 | 675 | } |
| 822 | 676 | |
| 823 | − response["content"] = contentBlocks | |
| 824 | − | |
| 825 | 677 | if respUsage := root.Get("usage"); respUsage.Exists() { |
| 826 | − usageJSON := `{}` | |
| 827 | − usageJSON, _ = sjson.Set(usageJSON, "input_tokens", respUsage.Get("prompt_tokens").Int()) | |
| 828 | − usageJSON, _ = sjson.Set(usageJSON, "output_tokens", respUsage.Get("completion_tokens").Int()) | |
| 829 | − parsedUsage := gjson.Parse(usageJSON).Value().(map[string]interface{}) | |
| 830 | − response["usage"] = parsedUsage | |
| 831 | − } else { | |
| 832 | − response["usage"] = `{"input_tokens":0,"output_tokens":0}` | |
| 678 | + out, _ = sjson.Set(out, "usage.input_tokens", respUsage.Get("prompt_tokens").Int()) | |
| 679 | + out, _ = sjson.Set(out, "usage.output_tokens", respUsage.Get("completion_tokens").Int()) | |
| 833 | 680 | } |
| 834 | 681 | |
| 835 | − if response["stop_reason"] == nil { | |
| 682 | + if !stopReasonSet { | |
| 836 | 683 | if hasToolCall { |
| 837 | − response["stop_reason"] = "tool_use" | |
| 684 | + out, _ = sjson.Set(out, "stop_reason", "tool_use") | |
| 838 | 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 { | |
| 844 | − if toolBlocks := response["content"].([]interface{}); len(toolBlocks) > 0 { | |
| 845 | − for _, block := range toolBlocks { | |
| 846 | − if m, ok := block.(map[string]interface{}); ok && m["type"] == "tool_use" { | |
| 847 | − hasToolCall = true | |
| 848 | − break | |
| 849 | − } | |
| 850 | − } | |
| 851 | − } | |
| 852 | − if hasToolCall { | |
| 853 | − response["stop_reason"] = "tool_use" | |
| 854 | − } | |
| 855 | − } | |
| 856 | − | |
| 857 | − responseJSON, err := json.Marshal(response) | |
| 858 | − if err != nil { | |
| 859 | − return "" | |
| 860 | − } | |
| 861 | − return string(responseJSON) | |
| 690 | + return out | |
| 862 | 691 | } |
| 863 | 692 | |
| 864 | 693 | func ClaudeTokenCount(ctx context.Context, count int64) string { |
| @@ -8,7 +8,6 @@ package gemini · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "crypto/rand" |
| 11 | − "encoding/json" | |
| 12 | 11 | "fmt" |
| 13 | 12 | "math/big" |
| 14 | 13 | "strings" |
| @@ -94,7 +93,6 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 79 unmodified lines | ||
| 94 | 93 | out, _ = sjson.Set(out, "stream", stream) |
| 95 | 94 | |
| 96 | 95 | // Process contents (Gemini messages) -> OpenAI messages |
| 97 | − var openAIMessages []interface{} | |
| 98 | 96 | var toolCallIDs []string // Track tool call IDs for matching with tool results |
| 99 | 97 | |
| 100 | 98 | // System instruction -> OpenAI system message |
| @@ -105,22 +103,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 4 unmodified lines | ||
| 105 | 103 | } |
| 106 | 104 | if systemInstruction.Exists() { |
| 107 | 105 | parts := systemInstruction.Get("parts") |
| 108 | − msg := map[string]interface{}{ | |
| 109 | − "role": "system", | |
| 110 | − "content": []interface{}{}, | |
| 111 | − } | |
| 112 | − | |
| 113 | − var aggregatedParts []interface{} | |
| 106 | + msg := `{"role":"system","content":[]}` | |
| 107 | + hasContent := false | |
| 114 | 108 | |
| 115 | 109 | if parts.Exists() && parts.IsArray() { |
| 116 | 110 | parts.ForEach(func(_, part gjson.Result) bool { |
| 117 | 111 | // Handle text parts |
| 118 | 112 | if text := part.Get("text"); text.Exists() { |
| 119 | − formattedText := text.String() | |
| 120 | − aggregatedParts = append(aggregatedParts, map[string]interface{}{ | |
| 121 | − "type": "text", | |
| 122 | − "text": formattedText, | |
| 123 | − }) | |
| 113 | + contentPart := `{"type":"text","text":""}` | |
| 114 | + contentPart, _ = sjson.Set(contentPart, "text", text.String()) | |
| 115 | + msg, _ = sjson.SetRaw(msg, "content.-1", contentPart) | |
| 116 | + hasContent = true | |
| 124 | 117 | } |
| 125 | 118 | |
| 126 | 119 | // Handle inline data (e.g., images) |
| @@ -132,20 +125,17 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 5 unmodified lines | ||
| 132 | 125 | data := inlineData.Get("data").String() |
| 133 | 126 | imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) |
| 134 | 127 | |
| 135 | − aggregatedParts = append(aggregatedParts, map[string]interface{}{ | |
| 136 | − "type": "image_url", | |
| 137 | − "image_url": map[string]interface{}{ | |
| 138 | − "url": imageURL, | |
| 139 | − }, | |
| 140 | − }) | |
| 128 | + contentPart := `{"type":"image_url","image_url":{"url":""}}` | |
| 129 | + contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL) | |
| 130 | + msg, _ = sjson.SetRaw(msg, "content.-1", contentPart) | |
| 131 | + hasContent = true | |
| 141 | 132 | } |
| 142 | 133 | return true |
| 143 | 134 | }) |
| 144 | 135 | } |
| 145 | 136 | |
| 146 | − if len(aggregatedParts) > 0 { | |
| 147 | − msg["content"] = aggregatedParts | |
| 148 | − openAIMessages = append(openAIMessages, msg) | |
| 137 | + if hasContent { | |
| 138 | + out, _ = sjson.SetRaw(out, "messages.-1", msg) | |
| 149 | 139 | } |
| 150 | 140 | } |
| 151 | 141 | |
| @@ -159,16 +149,15 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 7 unmodified lines | ||
| 159 | 149 | role = "assistant" |
| 160 | 150 | } |
| 161 | 151 | |
| 162 | − // Create OpenAI message | |
| 163 | − msg := map[string]interface{}{ | |
| 164 | − "role": role, | |
| 165 | − "content": "", | |
| 166 | − } | |
| 152 | + msg := `{"role":"","content":""}` | |
| 153 | + msg, _ = sjson.Set(msg, "role", role) | |
| 167 | 154 | |
| 168 | 155 | var textBuilder strings.Builder |
| 169 | − var aggregatedParts []interface{} | |
| 156 | + contentWrapper := `{"arr":[]}` | |
| 157 | + contentPartsCount := 0 | |
| 170 | 158 | onlyTextContent := true |
| 171 | − var toolCalls []interface{} | |
| 159 | + toolCallsWrapper := `{"arr":[]}` | |
| 160 | + toolCallsCount := 0 | |
| 172 | 161 | |
| 173 | 162 | if parts.Exists() && parts.IsArray() { |
| 174 | 163 | parts.ForEach(func(_, part gjson.Result) bool { |
| @@ -176,10 +165,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line | ||
| 176 | 165 | if text := part.Get("text"); text.Exists() { |
| 177 | 166 | formattedText := text.String() |
| 178 | 167 | textBuilder.WriteString(formattedText) |
| 179 | − aggregatedParts = append(aggregatedParts, map[string]interface{}{ | |
| 180 | − "type": "text", | |
| 181 | − "text": formattedText, | |
| 182 | − }) | |
| 168 | + contentPart := `{"type":"text","text":""}` | |
| 169 | + contentPart, _ = sjson.Set(contentPart, "text", formattedText) | |
| 170 | + contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart) | |
| 171 | + contentPartsCount++ | |
| 183 | 172 | } |
| 184 | 173 | |
| 185 | 174 | // Handle inline data (e.g., images) |
| @@ -193,12 +182,10 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 7 unmodified lines | ||
| 193 | 182 | data := inlineData.Get("data").String() |
| 194 | 183 | imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) |
| 195 | 184 | |
| 196 | − aggregatedParts = append(aggregatedParts, map[string]interface{}{ | |
| 197 | − "type": "image_url", | |
| 198 | − "image_url": map[string]interface{}{ | |
| 199 | − "url": imageURL, | |
| 200 | − }, | |
| 201 | − }) | |
| 185 | + contentPart := `{"type":"image_url","image_url":{"url":""}}` | |
| 186 | + contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL) | |
| 187 | + contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart) | |
| 188 | + contentPartsCount++ | |
| 202 | 189 | } |
| 203 | 190 | |
| 204 | 191 | // Handle function calls (Gemini) -> tool calls (OpenAI) |
| @@ -206,44 +193,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line | ||
| 206 | 193 | toolCallID := genToolCallID() |
| 207 | 194 | toolCallIDs = append(toolCallIDs, toolCallID) |
| 208 | 195 | |
| 209 | − toolCall := map[string]interface{}{ | |
| 210 | − "id": toolCallID, | |
| 211 | − "type": "function", | |
| 212 | − "function": map[string]interface{}{ | |
| 213 | − "name": functionCall.Get("name").String(), | |
| 214 | − }, | |
| 215 | − } | |
| 196 | + toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}` | |
| 197 | + toolCall, _ = sjson.Set(toolCall, "id", toolCallID) | |
| 198 | + toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String()) | |
| 216 | 199 | |
| 217 | 200 | // Convert args to arguments JSON string |
| 218 | 201 | if args := functionCall.Get("args"); args.Exists() { |
| 219 | − argsJSON, _ := json.Marshal(args.Value()) | |
| 220 | − toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON) | |
| 202 | + toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw) | |
| 221 | 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 | 211 | // Handle function responses (Gemini) -> tool role messages (OpenAI) |
| 229 | 212 | if functionResponse := part.Get("functionResponse"); functionResponse.Exists() { |
| 230 | 213 | // Create tool message for function response |
| 231 | − toolMsg := map[string]interface{}{ | |
| 232 | − "role": "tool", | |
| 233 | − "tool_call_id": "", // Will be set based on context | |
| 234 | − "content": "", | |
| 235 | − } | |
| 214 | + toolMsg := `{"role":"tool","tool_call_id":"","content":""}` | |
| 236 | 215 | |
| 237 | 216 | // Convert response.content to JSON string |
| 238 | 217 | if response := functionResponse.Get("response"); response.Exists() { |
| 239 | − if content = response.Get("content"); content.Exists() { | |
| 240 | − // Use the content field from the response | |
| 241 | − contentJSON, _ := json.Marshal(content.Value()) | |
| 242 | − toolMsg["content"] = string(contentJSON) | |
| 218 | + if contentField := response.Get("content"); contentField.Exists() { | |
| 219 | + toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw) | |
| 243 | 220 | } else { |
| 244 | − // Fallback to entire response | |
| 245 | − responseJSON, _ := json.Marshal(response.Value()) | |
| 246 | − toolMsg["content"] = string(responseJSON) | |
| 221 | + toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw) | |
| 247 | 222 | } |
| 248 | 223 | } |
| 249 | 224 | |
| @@ -252,13 +227,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 2 unmodified lines | ||
| 252 | 227 | if len(toolCallIDs) > 0 { |
| 253 | 228 | // Use the last tool call ID (simple matching by function name) |
| 254 | 229 | // 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 | 231 | } else { |
| 257 | 232 | // 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 | 239 | return true |
| @@ -266,170 +241,46 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream · 1 unmodified line | ||
| 266 | 241 | } |
| 267 | 242 | |
| 268 | 243 | // Set content |
| 269 | − if len(aggregatedParts) > 0 { | |
| 244 | + if contentPartsCount > 0 { | |
| 270 | 245 | if onlyTextContent { |
| 271 | − msg["content"] = textBuilder.String() | |
| 246 | + msg, _ = sjson.Set(msg, "content", textBuilder.String()) | |
| 272 | 247 | } else { |
| 273 | − msg["content"] = aggregatedParts | |
| 248 | + msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw) | |
| 274 | 249 | } |
| 275 | 250 | } |
| 276 | 251 | |
| 277 | 252 | // Set tool calls if any |
| 278 | − if len(toolCalls) > 0 { | |
| 279 | − msg["tool_calls"] = toolCalls | |
| 253 | + if toolCallsCount > 0 { | |
| 254 | + msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw) | |
| 280 | 255 | } |
| 281 | 256 | |
| 282 | − openAIMessages = append(openAIMessages, msg) | |
| 283 | − | |
| 284 | − // switch role { | |
| 285 | − // case "user", "model": | |
| 286 | − // // Convert role: model -> assistant | |
| 287 | − // if role == "model" { | |
| 288 | − // role = "assistant" | |
| 289 | − // } | |
| 290 | − // | |
| 291 | − // // Create OpenAI message | |
| 292 | − // msg := map[string]interface{}{ | |
| 293 | − // "role": role, | |
| 294 | − // "content": "", | |
| 295 | − // } | |
| 296 | − // | |
| 297 | − // var contentParts []string | |
| 298 | − // var toolCalls []interface{} | |
| 299 | − // | |
| 300 | − // if parts.Exists() && parts.IsArray() { | |
| 301 | − // parts.ForEach(func(_, part gjson.Result) bool { | |
| 302 | − // // Handle text parts | |
| 303 | − // if text := part.Get("text"); text.Exists() { | |
| 304 | − // contentParts = append(contentParts, text.String()) | |
| 305 | − // } | |
| 306 | − // | |
| 307 | − // // Handle function calls (Gemini) -> tool calls (OpenAI) | |
| 308 | − // if functionCall := part.Get("functionCall"); functionCall.Exists() { | |
| 309 | − // toolCallID := genToolCallID() | |
| 310 | − // toolCallIDs = append(toolCallIDs, toolCallID) | |
| 311 | − // | |
| 312 | − // toolCall := map[string]interface{}{ | |
| 313 | − // "id": toolCallID, | |
| 314 | − // "type": "function", | |
| 315 | − // "function": map[string]interface{}{ | |
| 316 | − // "name": functionCall.Get("name").String(), | |
| 317 | − // }, | |
| 318 | − // } | |
| 319 | − // | |
| 320 | − // // Convert args to arguments JSON string | |
| 321 | − // if args := functionCall.Get("args"); args.Exists() { | |
| 322 | − // argsJSON, _ := json.Marshal(args.Value()) | |
| 323 | − // toolCall["function"].(map[string]interface{})["arguments"] = string(argsJSON) | |
| 324 | − // } else { | |
| 325 | − // toolCall["function"].(map[string]interface{})["arguments"] = "{}" | |
| 326 | − // } | |
| 327 | − // | |
| 328 | − // toolCalls = append(toolCalls, toolCall) | |
| 329 | − // } | |
| 330 | − // | |
| 331 | − // return true | |
| 332 | − // }) | |
| 333 | − // } | |
| 334 | − // | |
| 335 | − // // Set content | |
| 336 | − // if len(contentParts) > 0 { | |
| 337 | − // msg["content"] = strings.Join(contentParts, "") | |
| 338 | − // } | |
| 339 | − // | |
| 340 | − // // Set tool calls if any | |
| 341 | − // if len(toolCalls) > 0 { | |
| 342 | − // msg["tool_calls"] = toolCalls | |
| 343 | − // } | |
| 344 | − // | |
| 345 | − // openAIMessages = append(openAIMessages, msg) | |
| 346 | − // | |
| 347 | − // case "function": | |
| 348 | − // // Handle Gemini function role -> OpenAI tool role | |
| 349 | − // if parts.Exists() && parts.IsArray() { | |
| 350 | − // parts.ForEach(func(_, part gjson.Result) bool { | |
| 351 | − // // Handle function responses (Gemini) -> tool role messages (OpenAI) | |
| 352 | − // if functionResponse := part.Get("functionResponse"); functionResponse.Exists() { | |
| 353 | − // // Create tool message for function response | |
| 354 | − // toolMsg := map[string]interface{}{ | |
| 355 | − // "role": "tool", | |
| 356 | − // "tool_call_id": "", // Will be set based on context | |
| 357 | − // "content": "", | |
| 358 | − // } | |
| 359 | − // | |
| 360 | − // // Convert response.content to JSON string | |
| 361 | − // if response := functionResponse.Get("response"); response.Exists() { | |
| 362 | − // if content = response.Get("content"); content.Exists() { | |
| 363 | − // // Use the content field from the response | |
| 364 | − // contentJSON, _ := json.Marshal(content.Value()) | |
| 365 | − // toolMsg["content"] = string(contentJSON) | |
| 366 | − // } else { | |
| 367 | − // // Fallback to entire response | |
| 368 | − // responseJSON, _ := json.Marshal(response.Value()) | |
| 369 | − // toolMsg["content"] = string(responseJSON) | |
| 370 | − // } | |
| 371 | − // } | |
| 372 | − // | |
| 373 | − // // Try to match with previous tool call ID | |
| 374 | − // _ = functionResponse.Get("name").String() // functionName not used for now | |
| 375 | − // if len(toolCallIDs) > 0 { | |
| 376 | − // // Use the last tool call ID (simple matching by function name) | |
| 377 | − // // In a real implementation, you might want more sophisticated matching | |
| 378 | − // toolMsg["tool_call_id"] = toolCallIDs[len(toolCallIDs)-1] | |
| 379 | − // } else { | |
| 380 | − // // Generate a tool call ID if none available | |
| 381 | − // toolMsg["tool_call_id"] = genToolCallID() | |
| 382 | − // } | |
| 383 | − // | |
| 384 | − // openAIMessages = append(openAIMessages, toolMsg) | |
| 385 | − // } | |
| 386 | − // | |
| 387 | − // return true | |
| 388 | − // }) | |
| 389 | − // } | |
| 390 | − // } | |
| 257 | + out, _ = sjson.SetRaw(out, "messages.-1", msg) | |
| 391 | 258 | 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 | 262 | // Tools mapping: Gemini tools -> OpenAI tools |
| 402 | 263 | if tools := root.Get("tools"); tools.Exists() && tools.IsArray() { |
| 403 | − var openAITools []interface{} | |
| 404 | 264 | tools.ForEach(func(_, tool gjson.Result) bool { |
| 405 | 265 | if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() { |
| 406 | 266 | functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool { |
| 407 | − openAITool := map[string]interface{}{ | |
| 408 | − "type": "function", | |
| 409 | − "function": map[string]interface{}{ | |
| 410 | − "name": funcDecl.Get("name").String(), | |
| 411 | − "description": funcDecl.Get("description").String(), | |
| 412 | − }, | |
| 413 | − } | |
| 267 | + openAITool := `{"type":"function","function":{"name":"","description":""}}` | |
| 268 | + openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String()) | |
| 269 | + openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String()) | |
| 414 | 270 | |
| 415 | 271 | // Convert parameters schema |
| 416 | 272 | if parameters := funcDecl.Get("parameters"); parameters.Exists() { |
| 417 | − openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value() | |
| 418 | − } else if parameters = funcDecl.Get("parametersJsonSchema"); parameters.Exists() { | |
| 419 | − openAITool["function"].(map[string]interface{})["parameters"] = parameters.Value() | |
| 273 | + openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw) | |
| 274 | + } else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() { | |
| 275 | + openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw) | |
| 420 | 276 | } |
| 421 | 277 | |
| 422 | − openAITools = append(openAITools, openAITool) | |
| 278 | + out, _ = sjson.SetRaw(out, "tools.-1", openAITool) | |
| 423 | 279 | return true |
| 424 | 280 | }) |
| 425 | 281 | } |
| 426 | 282 | 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 | 286 | // Tool choice mapping (Gemini doesn't have direct equivalent, but we can handle it) |
| @@ -8,7 +8,6 @@ package gemini · 7 unmodified lines | ||
| 8 | 8 | import ( |
| 9 | 9 | "bytes" |
| 10 | 10 | "context" |
| 11 | − "encoding/json" | |
| 12 | 11 | "fmt" |
| 13 | 12 | "strconv" |
| 14 | 13 | "strings" |
| @@ -84,15 +83,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 69 unmodified lines | ||
| 84 | 83 | template, _ = sjson.Set(template, "model", model.String()) |
| 85 | 84 | } |
| 86 | 85 | |
| 87 | − usageObj := map[string]interface{}{ | |
| 88 | − "promptTokenCount": usage.Get("prompt_tokens").Int(), | |
| 89 | − "candidatesTokenCount": usage.Get("completion_tokens").Int(), | |
| 90 | − "totalTokenCount": usage.Get("total_tokens").Int(), | |
| 91 | − } | |
| 86 | + template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) | |
| 87 | + template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) | |
| 88 | + template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) | |
| 92 | 89 | if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 { |
| 93 | − usageObj["thoughtsTokenCount"] = reasoningTokens | |
| 90 | + template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens) | |
| 94 | 91 | } |
| 95 | − template, _ = sjson.Set(template, "usageMetadata", usageObj) | |
| 96 | 92 | return []string{template} |
| 97 | 93 | } |
| 98 | 94 | return []string{} |
| @@ -133,13 +129,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 34 unmodified lines | ||
| 133 | 129 | continue |
| 134 | 130 | } |
| 135 | 131 | reasoningTemplate := baseTemplate |
| 136 | − parts := []interface{}{ | |
| 137 | − map[string]interface{}{ | |
| 138 | − "thought": true, | |
| 139 | − "text": reasoningText, | |
| 140 | − }, | |
| 141 | − } | |
| 142 | − reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts", parts) | |
| 132 | + reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true) | |
| 133 | + reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText) | |
| 143 | 134 | chunkOutputs = append(chunkOutputs, reasoningTemplate) |
| 144 | 135 | } |
| 145 | 136 | } |
| @@ -150,13 +141,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 4 unmodified lines | ||
| 150 | 141 | (*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText) |
| 151 | 142 | |
| 152 | 143 | // Create text part for this delta |
| 153 | − parts := []interface{}{ | |
| 154 | − map[string]interface{}{ | |
| 155 | − "text": contentText, | |
| 156 | − }, | |
| 157 | − } | |
| 158 | 144 | 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 | 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 | 212 | // If we have accumulated tool calls, output them now |
| 227 | 213 | if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 { |
| 228 | − var parts []interface{} | |
| 214 | + partIndex := 0 | |
| 229 | 215 | for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator { |
| 230 | − argsStr := accumulator.Arguments.String() | |
| 231 | − var argsMap map[string]interface{} | |
| 232 | − | |
| 233 | − argsMap = parseArgsToMap(argsStr) | |
| 234 | − | |
| 235 | − functionCallPart := map[string]interface{}{ | |
| 236 | − "functionCall": map[string]interface{}{ | |
| 237 | − "name": accumulator.Name, | |
| 238 | − "args": argsMap, | |
| 239 | − }, | |
| 240 | − } | |
| 241 | − parts = append(parts, functionCallPart) | |
| 242 | − } | |
| 243 | − | |
| 244 | − if len(parts) > 0 { | |
| 245 | − template, _ = sjson.Set(template, "candidates.0.content.parts", parts) | |
| 216 | + namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex) | |
| 217 | + argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex) | |
| 218 | + template, _ = sjson.Set(template, namePath, accumulator.Name) | |
| 219 | + template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String())) | |
| 220 | + partIndex++ | |
| 246 | 221 | } |
| 247 | 222 | |
| 248 | 223 | // Clear accumulators |
| @@ -255,15 +230,12 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR · 6 unmodified lines | ||
| 255 | 230 | |
| 256 | 231 | // Handle usage information |
| 257 | 232 | if usage := root.Get("usage"); usage.Exists() { |
| 258 | − usageObj := map[string]interface{}{ | |
| 259 | − "promptTokenCount": usage.Get("prompt_tokens").Int(), | |
| 260 | − "candidatesTokenCount": usage.Get("completion_tokens").Int(), | |
| 261 | − "totalTokenCount": usage.Get("total_tokens").Int(), | |
| 262 | − } | |
| 233 | + template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) | |
| 234 | + template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) | |
| 235 | + template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) | |
| 263 | 236 | if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 { |
| 264 | − usageObj["thoughtsTokenCount"] = reasoningTokens | |
| 237 | + template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens) | |
| 265 | 238 | } |
| 266 | − template, _ = sjson.Set(template, "usageMetadata", usageObj) | |
| 267 | 239 | results = append(results, template) |
| 268 | 240 | 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. | |
| 295 | −// It returns an empty map if the input is empty or cannot be parsed as a JSON object. | |
| 296 | −func parseArgsToMap(argsStr string) map[string]interface{} { | |
| 266 | +// parseArgsToObjectRaw safely parses a JSON string of function arguments into an object JSON string. | |
| 267 | +// It returns "{}" if the input is empty or cannot be parsed as a JSON object. | |
| 268 | +func parseArgsToObjectRaw(argsStr string) string { | |
| 297 | 269 | trimmed := strings.TrimSpace(argsStr) |
| 298 | 270 | if trimmed == "" || trimmed == "{}" { |
| 299 | − return map[string]interface{}{} | |
| 271 | + return "{}" | |
| 300 | 272 | } |
| 301 | 273 | |
| 302 | 274 | // First try strict JSON |
| 303 | − var out map[string]interface{} | |
| 304 | − if errUnmarshal := json.Unmarshal([]byte(trimmed), &out); errUnmarshal == nil { | |
| 305 | − return out | |
| 275 | + if gjson.Valid(trimmed) { | |
| 276 | + strict := gjson.Parse(trimmed) | |
| 277 | + if strict.IsObject() { | |
| 278 | + return strict.Raw | |
| 279 | + } | |
| 306 | 280 | } |
| 307 | 281 | |
| 308 | 282 | // Tolerant parse: handle streams where values are barewords (e.g., 北京, celsius) |
| 309 | − tolerant := tolerantParseJSONMap(trimmed) | |
| 310 | − if len(tolerant) > 0 { | |
| 283 | + tolerant := tolerantParseJSONObjectRaw(trimmed) | |
| 284 | + if tolerant != "{}" { | |
| 311 | 285 | return tolerant |
| 312 | 286 | } |
| 313 | 287 | |
| 314 | 288 | // 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, tolerating | |
| 292 | +func escapeSjsonPathKey(key string) string { | |
| 293 | + key = strings.ReplaceAll(key, `\`, `\\`) | |
| 294 | + key = strings.ReplaceAll(key, `.`, `\.`) | |
| 295 | + return key | |
| 296 | +} | |
| 297 | + | |
| 298 | +// tolerantParseJSONObjectRaw attempts to parse a JSON-like object string into a JSON object string, tolerating | |
| 319 | 299 | // bareword values (unquoted strings) commonly seen during streamed tool calls. |
| 320 | 300 | // Example input: {"location": 北京, "unit": celsius} |
| 321 | −func tolerantParseJSONMap(s string) map[string]interface{} { | |
| 301 | +func tolerantParseJSONObjectRaw(s string) string { | |
| 322 | 302 | // Ensure we operate within the outermost braces if present |
| 323 | 303 | start := strings.Index(s, "{") |
| 324 | 304 | end := strings.LastIndex(s, "}") |
| 325 | 305 | if start == -1 || end == -1 || start >= end { |
| 326 | − return map[string]interface{}{} | |
| 306 | + return "{}" | |
| 327 | 307 | } |
| 328 | 308 | content := s[start+1 : end] |
| 329 | 309 | |
| 330 | 310 | runes := []rune(content) |
| 331 | 311 | n := len(runes) |
| 332 | 312 | i := 0 |
| 333 | − result := make(map[string]interface{}) | |
| 313 | + result := "{}" | |
| 334 | 314 | |
| 335 | 315 | for i < n { |
| 336 | 316 | // Skip whitespace and commas |
| @@ -356,6 +336,7 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 19 unmodified lines | ||
| 356 | 336 | break |
| 357 | 337 | } |
| 358 | 338 | keyName := jsonStringTokenToRawString(keyToken) |
| 339 | + sjsonKey := escapeSjsonPathKey(keyName) | |
| 359 | 340 | i = nextIdx |
| 360 | 341 | |
| 361 | 342 | // Skip whitespace |
| @@ -375,17 +356,16 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 13 unmodified lines | ||
| 375 | 356 | } |
| 376 | 357 | |
| 377 | 358 | // Parse value (string, number, object/array, bareword) |
| 378 | − var value interface{} | |
| 379 | 359 | switch runes[i] { |
| 380 | 360 | case '"': |
| 381 | 361 | // JSON string |
| 382 | 362 | valToken, ni := parseJSONStringRunes(runes, i) |
| 383 | 363 | if ni == -1 { |
| 384 | 364 | // Malformed; treat as empty string |
| 385 | − value = "" | |
| 365 | + result, _ = sjson.Set(result, sjsonKey, "") | |
| 386 | 366 | i = n |
| 387 | 367 | } else { |
| 388 | − value = jsonStringTokenToRawString(valToken) | |
| 368 | + result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken)) | |
| 389 | 369 | i = ni |
| 390 | 370 | } |
| 391 | 371 | case '{', '[': |
| @@ -394,11 +374,10 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 2 unmodified lines | ||
| 394 | 374 | if ni == -1 { |
| 395 | 375 | i = n |
| 396 | 376 | } else { |
| 397 | − var anyVal interface{} | |
| 398 | − if errUnmarshal := json.Unmarshal([]byte(seg), &anyVal); errUnmarshal == nil { | |
| 399 | − value = anyVal | |
| 377 | + if gjson.Valid(seg) { | |
| 378 | + result, _ = sjson.SetRaw(result, sjsonKey, seg) | |
| 400 | 379 | } else { |
| 401 | − value = seg | |
| 380 | + result, _ = sjson.Set(result, sjsonKey, seg) | |
| 402 | 381 | } |
| 403 | 382 | i = ni |
| 404 | 383 | } |
| @@ -411,21 +390,19 @@ func tolerantParseJSONMap(s string) map[string]interface{} { · 6 unmodified lines | ||
| 411 | 390 | token := strings.TrimSpace(string(runes[i:j])) |
| 412 | 391 | // Interpret common JSON atoms and numbers; otherwise treat as string |
| 413 | 392 | if token == "true" { |
| 414 | − value = true | |
| 393 | + result, _ = sjson.Set(result, sjsonKey, true) | |
| 415 | 394 | } else if token == "false" { |
| 416 | − value = false | |
| 395 | + result, _ = sjson.Set(result, sjsonKey, false) | |
| 417 | 396 | } else if token == "null" { |
| 418 | − value = nil | |
| 397 | + result, _ = sjson.Set(result, sjsonKey, nil) | |
| 419 | 398 | } else if numVal, ok := tryParseNumber(token); ok { |
| 420 | − value = numVal | |
| 399 | + result, _ = sjson.Set(result, sjsonKey, numVal) | |
| 421 | 400 | } else { |
| 422 | − value = token | |
| 401 | + result, _ = sjson.Set(result, sjsonKey, token) | |
| 423 | 402 | } |
| 424 | 403 | i = j |
| 425 | 404 | } |
| 426 | 405 | |
| 427 | − result[keyName] = value | |
| 428 | − | |
| 429 | 406 | // Skip trailing whitespace and optional comma before next pair |
| 430 | 407 | for i < n && (runes[i] == ' ' || runes[i] == '\n' || runes[i] == '\r' || runes[i] == '\t') { |
| 431 | 408 | i++ |
| @@ -463,9 +440,9 @@ func parseJSONStringRunes(runes []rune, start int) (string, int) { · 31 unmodified lines | ||
| 463 | 440 | |
| 464 | 441 | // jsonStringTokenToRawString converts a JSON string token (including quotes) to a raw Go string value. |
| 465 | 442 | func jsonStringTokenToRawString(token string) string { |
| 466 | − var s string | |
| 467 | − if errUnmarshal := json.Unmarshal([]byte(token), &s); errUnmarshal == nil { | |
| 468 | − return s | |
| 443 | + r := gjson.Parse(token) | |
| 444 | + if r.Type == gjson.String { | |
| 445 | + return r.String() | |
| 469 | 446 | } |
| 470 | 447 | // Fallback: strip surrounding quotes if present |
| 471 | 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 | 561 | // Handle reasoning content before visible text |
| 585 | 562 | if reasoning := message.Get("reasoning_content"); reasoning.Exists() { |
| @@ -587,18 +564,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 1 unmodified line | ||
| 587 | 564 | if reasoningText == "" { |
| 588 | 565 | continue |
| 589 | 566 | } |
| 590 | − parts = append(parts, map[string]interface{}{ | |
| 591 | − "thought": true, | |
| 592 | − "text": reasoningText, | |
| 593 | − }) | |
| 567 | + out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true) | |
| 568 | + out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText) | |
| 569 | + partIndex++ | |
| 594 | 570 | } |
| 595 | 571 | } |
| 596 | 572 | |
| 597 | 573 | // Handle content first |
| 598 | 574 | if content := message.Get("content"); content.Exists() && content.String() != "" { |
| 599 | − parts = append(parts, map[string]interface{}{ | |
| 600 | − "text": content.String(), | |
| 601 | − }) | |
| 575 | + out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String()) | |
| 576 | + partIndex++ | |
| 602 | 577 | } |
| 603 | 578 | |
| 604 | 579 | // Handle tool calls |
| @@ -609,27 +584,16 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 4 unmodified lines | ||
| 609 | 584 | functionName := function.Get("name").String() |
| 610 | 585 | functionArgs := function.Get("arguments").String() |
| 611 | 586 | |
| 612 | − // Parse arguments | |
| 613 | − var argsMap map[string]interface{} | |
| 614 | − argsMap = parseArgsToMap(functionArgs) | |
| 615 | − | |
| 616 | − functionCallPart := map[string]interface{}{ | |
| 617 | − "functionCall": map[string]interface{}{ | |
| 618 | − "name": functionName, | |
| 619 | − "args": argsMap, | |
| 620 | − }, | |
| 621 | − } | |
| 622 | − parts = append(parts, functionCallPart) | |
| 587 | + namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex) | |
| 588 | + argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex) | |
| 589 | + out, _ = sjson.Set(out, namePath, functionName) | |
| 590 | + out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs)) | |
| 591 | + partIndex++ | |
| 623 | 592 | } |
| 624 | 593 | 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 | 597 | // Handle finish reason |
| 634 | 598 | if finishReason := choice.Get("finish_reason"); finishReason.Exists() { |
| 635 | 599 | geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String()) |
| @@ -645,15 +609,12 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina · 9 unmodified lines | ||
| 645 | 609 | |
| 646 | 610 | // Handle usage information |
| 647 | 611 | if usage := root.Get("usage"); usage.Exists() { |
| 648 | − usageObj := map[string]interface{}{ | |
| 649 | − "promptTokenCount": usage.Get("prompt_tokens").Int(), | |
| 650 | − "candidatesTokenCount": usage.Get("completion_tokens").Int(), | |
| 651 | − "totalTokenCount": usage.Get("total_tokens").Int(), | |
| 652 | − } | |
| 612 | + out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) | |
| 613 | + out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) | |
| 614 | + out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) | |
| 653 | 615 | if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 { |
| 654 | − usageObj["thoughtsTokenCount"] = reasoningTokens | |
| 616 | + out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens) | |
| 655 | 617 | } |
| 656 | − out, _ = sjson.Set(out, "usageMetadata", usageObj) | |
| 657 | 618 | } |
| 658 | 619 | |
| 659 | 620 | return out |
| @@ -484,16 +484,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 483 unmodified lines | ||
| 484 | 484 | } |
| 485 | 485 | } |
| 486 | 486 | // Build response.output using aggregated buffers |
| 487 | − var outputs []interface{} | |
| 487 | + outputsWrapper := `{"arr":[]}` | |
| 488 | 488 | if st.ReasoningBuf.Len() > 0 { |
| 489 | − outputs = append(outputs, map[string]interface{}{ | |
| 490 | − "id": st.ReasoningID, | |
| 491 | − "type": "reasoning", | |
| 492 | − "summary": []interface{}{map[string]interface{}{ | |
| 493 | − "type": "summary_text", | |
| 494 | − "text": st.ReasoningBuf.String(), | |
| 495 | − }}, | |
| 496 | − }) | |
| 489 | + item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` | |
| 490 | + item, _ = sjson.Set(item, "id", st.ReasoningID) | |
| 491 | + item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String()) | |
| 492 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 497 | 493 | } |
| 498 | 494 | // Append message items in ascending index order |
| 499 | 495 | if len(st.MsgItemAdded) > 0 { |
| @@ -513,18 +509,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 13 unmodified lines | ||
| 513 | 509 | if b := st.MsgTextBuf[i]; b != nil { |
| 514 | 510 | txt = b.String() |
| 515 | 511 | } |
| 516 | − outputs = append(outputs, map[string]interface{}{ | |
| 517 | − "id": fmt.Sprintf("msg_%s_%d", st.ResponseID, i), | |
| 518 | − "type": "message", | |
| 519 | − "status": "completed", | |
| 520 | − "content": []interface{}{map[string]interface{}{ | |
| 521 | − "type": "output_text", | |
| 522 | − "annotations": []interface{}{}, | |
| 523 | − "logprobs": []interface{}{}, | |
| 524 | − "text": txt, | |
| 525 | − }}, | |
| 526 | − "role": "assistant", | |
| 527 | − }) | |
| 512 | + item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` | |
| 513 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) | |
| 514 | + item, _ = sjson.Set(item, "content.0.text", txt) | |
| 515 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 528 | 516 | } |
| 529 | 517 | } |
| 530 | 518 | if len(st.FuncArgsBuf) > 0 { |
| @@ -547,18 +535,16 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, · 16 unmodified lines | ||
| 547 | 535 | } |
| 548 | 536 | callID := st.FuncCallIDs[i] |
| 549 | 537 | name := st.FuncNames[i] |
| 550 | − outputs = append(outputs, map[string]interface{}{ | |
| 551 | − "id": fmt.Sprintf("fc_%s", callID), | |
| 552 | − "type": "function_call", | |
| 553 | − "status": "completed", | |
| 554 | − "arguments": args, | |
| 555 | − "call_id": callID, | |
| 556 | − "name": name, | |
| 557 | − }) | |
| 538 | + item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` | |
| 539 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) | |
| 540 | + item, _ = sjson.Set(item, "arguments", args) | |
| 541 | + item, _ = sjson.Set(item, "call_id", callID) | |
| 542 | + item, _ = sjson.Set(item, "name", name) | |
| 543 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 558 | 544 | } |
| 559 | 545 | } |
| 560 | − if len(outputs) > 0 { | |
| 561 | − completed, _ = sjson.Set(completed, "response.output", outputs) | |
| 546 | + if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { | |
| 547 | + completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw) | |
| 562 | 548 | } |
| 563 | 549 | if st.UsageSeen { |
| 564 | 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 | 669 | // Build output list from choices[...] |
| 684 | − var outputs []interface{} | |
| 670 | + outputsWrapper := `{"arr":[]}` | |
| 685 | 671 | // Detect and capture reasoning content if present |
| 686 | 672 | rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String() |
| 687 | 673 | includeReasoning := rcText != "" |
| @@ -693,21 +679,14 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 5 unmodified lines | ||
| 693 | 679 | if strings.HasPrefix(rid, "resp_") { |
| 694 | 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 | 682 | // 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 | 685 | if rcText != "" { |
| 704 | − summaries = append(summaries, map[string]interface{}{ | |
| 705 | − "type": "summary_text", | |
| 706 | − "text": rcText, | |
| 707 | − }) | |
| 686 | + reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text") | |
| 687 | + reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText) | |
| 708 | 688 | } |
| 709 | − reasoningItem["summary"] = summaries | |
| 710 | − outputs = append(outputs, reasoningItem) | |
| 689 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem) | |
| 711 | 690 | } |
| 712 | 691 | |
| 713 | 692 | if choices := root.Get("choices"); choices.Exists() && choices.IsArray() { |
| @@ -716,18 +695,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 2 unmodified lines | ||
| 716 | 695 | if msg.Exists() { |
| 717 | 696 | // Text message part |
| 718 | 697 | if c := msg.Get("content"); c.Exists() && c.String() != "" { |
| 719 | − outputs = append(outputs, map[string]interface{}{ | |
| 720 | − "id": fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())), | |
| 721 | − "type": "message", | |
| 722 | − "status": "completed", | |
| 723 | − "content": []interface{}{map[string]interface{}{ | |
| 724 | − "type": "output_text", | |
| 725 | − "annotations": []interface{}{}, | |
| 726 | − "logprobs": []interface{}{}, | |
| 727 | − "text": c.String(), | |
| 728 | − }}, | |
| 729 | − "role": "assistant", | |
| 730 | − }) | |
| 698 | + item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` | |
| 699 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int()))) | |
| 700 | + item, _ = sjson.Set(item, "content.0.text", c.String()) | |
| 701 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 731 | 702 | } |
| 732 | 703 | |
| 733 | 704 | // Function/tool calls |
| @@ -736,14 +707,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 2 unmodified lines | ||
| 736 | 707 | callID := tc.Get("id").String() |
| 737 | 708 | name := tc.Get("function.name").String() |
| 738 | 709 | args := tc.Get("function.arguments").String() |
| 739 | − outputs = append(outputs, map[string]interface{}{ | |
| 740 | − "id": fmt.Sprintf("fc_%s", callID), | |
| 741 | − "type": "function_call", | |
| 742 | − "status": "completed", | |
| 743 | − "arguments": args, | |
| 744 | − "call_id": callID, | |
| 745 | − "name": name, | |
| 746 | − }) | |
| 710 | + item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` | |
| 711 | + item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) | |
| 712 | + item, _ = sjson.Set(item, "arguments", args) | |
| 713 | + item, _ = sjson.Set(item, "call_id", callID) | |
| 714 | + item, _ = sjson.Set(item, "name", name) | |
| 715 | + outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) | |
| 747 | 716 | return true |
| 748 | 717 | }) |
| 749 | 718 | } |
| @@ -751,8 +720,8 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co · 1 unmodified line | ||
| 751 | 720 | return true |
| 752 | 721 | }) |
| 753 | 722 | } |
| 754 | − if len(outputs) > 0 { | |
| 755 | − resp, _ = sjson.Set(resp, "output", outputs) | |
| 723 | + if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { | |
| 724 | + resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw) | |
| 756 | 725 | } |
| 757 | 726 | |
| 758 | 727 | // usage mapping |
| @@ -17,6 +17,7 @@ import ( · 16 unmodified lines | ||
| 17 | 17 | . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" |
| 18 | 18 | "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
| 19 | 19 | "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" |
| 20 | + responsesconverter "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/openai/responses" | |
| 20 | 21 | "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" |
| 21 | 22 | "github.com/tidwall/gjson" |
| 22 | 23 | "github.com/tidwall/sjson" |
| @@ -109,7 +110,17 @@ func (h *OpenAIAPIHandler) ChatCompletions(c *gin.Context) { · 86 unmodified lines | ||
| 109 | 110 | |
| 110 | 111 | // Check if the client requested a streaming response. |
| 111 | 112 | streamResult := gjson.GetBytes(rawJSON, "stream") |
| 112 | − if streamResult.Type == gjson.True { | |
| 113 | + stream := streamResult.Type == gjson.True | |
| 114 | + | |
| 115 | + // Some clients send OpenAI Responses-format payloads to /v1/chat/completions. | |
| 116 | + // Convert them to Chat Completions so downstream translators preserve tool metadata. | |
| 117 | + if shouldTreatAsResponsesFormat(rawJSON) { | |
| 118 | + modelName := gjson.GetBytes(rawJSON, "model").String() | |
| 119 | + rawJSON = responsesconverter.ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName, rawJSON, stream) | |
| 120 | + stream = gjson.GetBytes(rawJSON, "stream").Bool() | |
| 121 | + } | |
| 122 | + | |
| 123 | + if stream { | |
| 113 | 124 | h.handleStreamingResponse(c, rawJSON) |
| 114 | 125 | } else { |
| 115 | 126 | h.handleNonStreamingResponse(c, rawJSON) |
| @@ -117,6 +128,21 @@ func (h *OpenAIAPIHandler) ChatCompletions(c *gin.Context) { · 1 unmodified line | ||
| 117 | 128 | |
| 118 | 129 | } |
| 119 | 130 | |
| 131 | +// shouldTreatAsResponsesFormat detects OpenAI Responses-style payloads that are | |
| 132 | +// accidentally sent to the Chat Completions endpoint. | |
| 133 | +func shouldTreatAsResponsesFormat(rawJSON []byte) bool { | |
| 134 | + if gjson.GetBytes(rawJSON, "messages").Exists() { | |
| 135 | + return false | |
| 136 | + } | |
| 137 | + if gjson.GetBytes(rawJSON, "input").Exists() { | |
| 138 | + return true | |
| 139 | + } | |
| 140 | + if gjson.GetBytes(rawJSON, "instructions").Exists() { | |
| 141 | + return true | |
| 142 | + } | |
| 143 | + return false | |
| 144 | +} | |
| 145 | + | |
| 120 | 146 | // Completions handles the /v1/completions endpoint. |
| 121 | 147 | // It determines whether the request is for a streaming or non-streaming response |
| 122 | 148 | // and calls the appropriate handler based on the model provider. |