diff --git a/cmd/server/main.go b/cmd/server/main.go index 3d9ee6cf..e12e5261 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -74,6 +74,7 @@ func main() { var password string var tuiMode bool var standalone bool + var localModel bool // Define command-line flags for different operation modes. flag.BoolVar(&login, "login", false, "Login Google Account") @@ -93,6 +94,7 @@ func main() { flag.StringVar(&password, "password", "", "") flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI") flag.BoolVar(&standalone, "standalone", false, "In TUI mode, start an embedded local server") + flag.BoolVar(&localModel, "local-model", false, "Use embedded model catalog only, skip remote model fetching") flag.CommandLine.Usage = func() { out := flag.CommandLine.Output() @@ -491,11 +493,16 @@ func main() { cmd.WaitForCloudDeploy() return } + if localModel && (!tuiMode || standalone) { + log.Info("Local model mode: using embedded model catalog, remote model updates disabled") + } if tuiMode { if standalone { // Standalone mode: start an embedded local server and connect TUI client to it. managementasset.StartAutoUpdater(context.Background(), configFilePath) - registry.StartModelsUpdater(context.Background()) + if !localModel { + registry.StartModelsUpdater(context.Background()) + } hook := tui.NewLogHook(2000) hook.SetFormatter(&logging.LogFormatter{}) log.AddHook(hook) @@ -568,7 +575,9 @@ func main() { } else { // Start the main proxy service managementasset.StartAutoUpdater(context.Background(), configFilePath) - registry.StartModelsUpdater(context.Background()) + if !localModel { + registry.StartModelsUpdater(context.Background()) + } cmd.StartService(cfg, configFilePath, password) } } diff --git a/examples/custom-provider/main.go b/examples/custom-provider/main.go index 7c611f9e..fdbae275 100644 --- a/examples/custom-provider/main.go +++ b/examples/custom-provider/main.go @@ -52,11 +52,11 @@ func init() { sdktr.Register(fOpenAI, fMyProv, func(model string, raw []byte, stream bool) []byte { return raw }, sdktr.ResponseTransform{ - Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []string { - return []string{string(raw)} + Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) [][]byte { + return [][]byte{raw} }, - NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) string { - return string(raw) + NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []byte { + return raw }, }, ) diff --git a/internal/auth/gemini/gemini_auth.go b/internal/auth/gemini/gemini_auth.go index c459c5ca..2995a1cb 100644 --- a/internal/auth/gemini/gemini_auth.go +++ b/internal/auth/gemini/gemini_auth.go @@ -305,6 +305,9 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config, defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -326,13 +329,14 @@ waitForCallback: return nil, err default: } - input, err := opts.Prompt("Paste the Gemini callback URL (or press Enter to keep waiting): ") - if err != nil { - return nil, err - } - parsed, err := misc.ParseOAuthCallback(input) - if err != nil { - return nil, err + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Gemini callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil + parsed, errParse := misc.ParseOAuthCallback(input) + if errParse != nil { + return nil, errParse } if parsed == nil { continue @@ -345,6 +349,8 @@ waitForCallback: } authCode = parsed.Code break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual case <-timeoutTimer.C: return nil, fmt.Errorf("oauth flow timed out") } diff --git a/internal/misc/oauth.go b/internal/misc/oauth.go index c14f39d2..88be2eef 100644 --- a/internal/misc/oauth.go +++ b/internal/misc/oauth.go @@ -30,6 +30,23 @@ type OAuthCallback struct { ErrorDescription string } +// AsyncPrompt runs a prompt function in a goroutine and returns channels for +// the result. The returned channels are buffered (size 1) so the goroutine can +// complete even if the caller abandons the channels. +func AsyncPrompt(promptFn func(string) (string, error), message string) (<-chan string, <-chan error) { + inputCh := make(chan string, 1) + errCh := make(chan error, 1) + go func() { + input, err := promptFn(message) + if err != nil { + errCh <- err + return + } + inputCh <- input + }() + return inputCh, errCh +} + // ParseOAuthCallback extracts OAuth parameters from a callback URL. // It returns nil when the input is empty. func ParseOAuthCallback(input string) (*OAuthCallback, error) { diff --git a/internal/runtime/executor/aistudio_executor.go b/internal/runtime/executor/aistudio_executor.go index b1e23860..db56a183 100644 --- a/internal/runtime/executor/aistudio_executor.go +++ b/internal/runtime/executor/aistudio_executor.go @@ -164,7 +164,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, reporter.publish(ctx, parseGeminiUsage(wsResp.Body)) var param any out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, ¶m) - resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON([]byte(out)), Headers: wsResp.Headers.Clone()} + resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON(out), Headers: wsResp.Headers.Clone()} return resp, nil } @@ -280,7 +280,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth } lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))} + out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])} } break } @@ -296,7 +296,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth } lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))} + out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])} } reporter.publish(ctx, parseGeminiUsage(event.Payload)) return false @@ -373,7 +373,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A return cliproxyexecutor.Response{}, fmt.Errorf("wsrelay: totalTokens missing in response") } translated := sdktranslator.TranslateTokenCount(ctx, body.toFormat, opts.SourceFormat, totalTokens, resp.Body) - return cliproxyexecutor.Response{Payload: []byte(translated)}, nil + return cliproxyexecutor.Response{Payload: translated}, nil } // Refresh refreshes the authentication credentials (no-op for AI Studio). diff --git a/internal/runtime/executor/antigravity_executor.go b/internal/runtime/executor/antigravity_executor.go index cda02d2c..18079a43 100644 --- a/internal/runtime/executor/antigravity_executor.go +++ b/internal/runtime/executor/antigravity_executor.go @@ -308,7 +308,7 @@ attemptLoop: reporter.publish(ctx, parseAntigravityUsage(bodyBytes)) var param any converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(converted), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()} reporter.ensurePublished(ctx) return resp, nil } @@ -512,7 +512,7 @@ attemptLoop: reporter.publish(ctx, parseAntigravityUsage(resp.Payload)) var param any converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(converted), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()} reporter.ensurePublished(ctx) return resp, nil @@ -691,31 +691,42 @@ func (e *AntigravityExecutor) convertStreamToNonStream(stream []byte) []byte { } partsJSON, _ := json.Marshal(parts) - responseTemplate, _ = sjson.SetRaw(responseTemplate, "candidates.0.content.parts", string(partsJSON)) + updatedTemplate, _ := sjson.SetRawBytes([]byte(responseTemplate), "candidates.0.content.parts", partsJSON) + responseTemplate = string(updatedTemplate) if role != "" { - responseTemplate, _ = sjson.Set(responseTemplate, "candidates.0.content.role", role) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "candidates.0.content.role", role) + responseTemplate = string(updatedTemplate) } if finishReason != "" { - responseTemplate, _ = sjson.Set(responseTemplate, "candidates.0.finishReason", finishReason) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "candidates.0.finishReason", finishReason) + responseTemplate = string(updatedTemplate) } if modelVersion != "" { - responseTemplate, _ = sjson.Set(responseTemplate, "modelVersion", modelVersion) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "modelVersion", modelVersion) + responseTemplate = string(updatedTemplate) } if responseID != "" { - responseTemplate, _ = sjson.Set(responseTemplate, "responseId", responseID) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "responseId", responseID) + responseTemplate = string(updatedTemplate) } if usageRaw != "" { - responseTemplate, _ = sjson.SetRaw(responseTemplate, "usageMetadata", usageRaw) + updatedTemplate, _ = sjson.SetRawBytes([]byte(responseTemplate), "usageMetadata", []byte(usageRaw)) + responseTemplate = string(updatedTemplate) } else if !gjson.Get(responseTemplate, "usageMetadata").Exists() { - responseTemplate, _ = sjson.Set(responseTemplate, "usageMetadata.promptTokenCount", 0) - responseTemplate, _ = sjson.Set(responseTemplate, "usageMetadata.candidatesTokenCount", 0) - responseTemplate, _ = sjson.Set(responseTemplate, "usageMetadata.totalTokenCount", 0) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "usageMetadata.promptTokenCount", 0) + responseTemplate = string(updatedTemplate) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "usageMetadata.candidatesTokenCount", 0) + responseTemplate = string(updatedTemplate) + updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "usageMetadata.totalTokenCount", 0) + responseTemplate = string(updatedTemplate) } output := `{"response":{},"traceId":""}` - output, _ = sjson.SetRaw(output, "response", responseTemplate) + updatedOutput, _ := sjson.SetRawBytes([]byte(output), "response", []byte(responseTemplate)) + output = string(updatedOutput) if traceID != "" { - output, _ = sjson.Set(output, "traceId", traceID) + updatedOutput, _ = sjson.SetBytes([]byte(output), "traceId", traceID) + output = string(updatedOutput) } return []byte(output) } @@ -880,12 +891,12 @@ attemptLoop: chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } tail := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, []byte("[DONE]"), ¶m) for i := range tail { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(tail[i])} + out <- cliproxyexecutor.StreamChunk{Payload: tail[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) @@ -1043,7 +1054,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices { count := gjson.GetBytes(bodyBytes, "totalTokens").Int() translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, bodyBytes) - return cliproxyexecutor.Response{Payload: []byte(translated), Headers: httpResp.Header.Clone()}, nil + return cliproxyexecutor.Response{Payload: translated, Headers: httpResp.Header.Clone()}, nil } lastStatus = httpResp.StatusCode @@ -1265,19 +1276,20 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau // if useAntigravitySchema { // systemInstructionPartsResult := gjson.Get(payloadStr, "request.systemInstruction.parts") - // payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.role", "user") - // payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.0.text", systemInstruction) - // payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction)) + // payloadStr, _ = sjson.SetBytes([]byte(payloadStr), "request.systemInstruction.role", "user") + // payloadStr, _ = sjson.SetBytes([]byte(payloadStr), "request.systemInstruction.parts.0.text", systemInstruction) + // payloadStr, _ = sjson.SetBytes([]byte(payloadStr), "request.systemInstruction.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction)) // if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() { // for _, partResult := range systemInstructionPartsResult.Array() { - // payloadStr, _ = sjson.SetRaw(payloadStr, "request.systemInstruction.parts.-1", partResult.Raw) + // payloadStr, _ = sjson.SetRawBytes([]byte(payloadStr), "request.systemInstruction.parts.-1", []byte(partResult.Raw)) // } // } // } if strings.Contains(modelName, "claude") { - payloadStr, _ = sjson.Set(payloadStr, "request.toolConfig.functionCallingConfig.mode", "VALIDATED") + updated, _ := sjson.SetBytes([]byte(payloadStr), "request.toolConfig.functionCallingConfig.mode", "VALIDATED") + payloadStr = string(updated) } else { payloadStr, _ = sjson.Delete(payloadStr, "request.generationConfig.maxOutputTokens") } @@ -1499,8 +1511,9 @@ func resolveCustomAntigravityBaseURL(auth *cliproxyauth.Auth) string { } func geminiToAntigravity(modelName string, payload []byte, projectID string) []byte { - template, _ := sjson.Set(string(payload), "model", modelName) - template, _ = sjson.Set(template, "userAgent", "antigravity") + template := payload + template, _ = sjson.SetBytes(template, "model", modelName) + template, _ = sjson.SetBytes(template, "userAgent", "antigravity") isImageModel := strings.Contains(modelName, "image") @@ -1510,28 +1523,28 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b } else { reqType = "agent" } - template, _ = sjson.Set(template, "requestType", reqType) + template, _ = sjson.SetBytes(template, "requestType", reqType) // Use real project ID from auth if available, otherwise generate random (legacy fallback) if projectID != "" { - template, _ = sjson.Set(template, "project", projectID) + template, _ = sjson.SetBytes(template, "project", projectID) } else { - template, _ = sjson.Set(template, "project", generateProjectID()) + template, _ = sjson.SetBytes(template, "project", generateProjectID()) } if isImageModel { - template, _ = sjson.Set(template, "requestId", generateImageGenRequestID()) + template, _ = sjson.SetBytes(template, "requestId", generateImageGenRequestID()) } else { - template, _ = sjson.Set(template, "requestId", generateRequestID()) - template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload)) + template, _ = sjson.SetBytes(template, "requestId", generateRequestID()) + template, _ = sjson.SetBytes(template, "request.sessionId", generateStableSessionID(payload)) } - template, _ = sjson.Delete(template, "request.safetySettings") - if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() { - template, _ = sjson.SetRaw(template, "request.toolConfig", toolConfig.Raw) - template, _ = sjson.Delete(template, "toolConfig") + template, _ = sjson.DeleteBytes(template, "request.safetySettings") + if toolConfig := gjson.GetBytes(template, "toolConfig"); toolConfig.Exists() && !gjson.GetBytes(template, "request.toolConfig").Exists() { + template, _ = sjson.SetRawBytes(template, "request.toolConfig", []byte(toolConfig.Raw)) + template, _ = sjson.DeleteBytes(template, "toolConfig") } - return []byte(template) + return template } func generateRequestID() string { diff --git a/internal/runtime/executor/claude_executor.go b/internal/runtime/executor/claude_executor.go index 82b12a2f..dff36230 100644 --- a/internal/runtime/executor/claude_executor.go +++ b/internal/runtime/executor/claude_executor.go @@ -255,7 +255,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r data, ¶m, ) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -443,7 +443,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A ¶m, ) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } if errScan := scanner.Err(); errScan != nil { @@ -561,7 +561,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut appendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "input_tokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) - return cliproxyexecutor.Response{Payload: []byte(out), Headers: resp.Header.Clone()}, nil + return cliproxyexecutor.Response{Payload: out, Headers: resp.Header.Clone()}, nil } func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { @@ -1260,7 +1260,8 @@ func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte { // TTL ordering violations with the prompt-caching-scope-2026-01-05 beta. partJSON := part.Raw if !part.Get("cache_control").Exists() { - partJSON, _ = sjson.Set(partJSON, "cache_control.type", "ephemeral") + updated, _ := sjson.SetBytes([]byte(partJSON), "cache_control.type", "ephemeral") + partJSON = string(updated) } result += "," + partJSON } @@ -1268,7 +1269,8 @@ func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte { }) } else if system.Type == gjson.String && system.String() != "" { partJSON := `{"type":"text","cache_control":{"type":"ephemeral"}}` - partJSON, _ = sjson.Set(partJSON, "text", system.String()) + updated, _ := sjson.SetBytes([]byte(partJSON), "text", system.String()) + partJSON = string(updated) result += "," + partJSON } result += "]" diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 4fb22919..e6f75b5d 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -183,7 +183,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, line, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } err = statusErr{code: 408, msg: "stream error: stream disconnected before completion: stream closed before response.completed"} @@ -273,7 +273,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A reporter.ensurePublished(ctx) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -387,7 +387,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, body, bytes.Clone(line), ¶m) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } if errScan := scanner.Err(); errScan != nil { @@ -432,7 +432,7 @@ func (e *CodexExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth usageJSON := fmt.Sprintf(`{"response":{"usage":{"input_tokens":%d,"output_tokens":0,"total_tokens":%d}}}`, count, count) translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, []byte(usageJSON)) - return cliproxyexecutor.Response{Payload: []byte(translated)}, nil + return cliproxyexecutor.Response{Payload: translated}, nil } func tokenizerForCodexModel(model string) (tokenizer.Codec, error) { diff --git a/internal/runtime/executor/codex_websockets_executor.go b/internal/runtime/executor/codex_websockets_executor.go index 571a23a1..3ea88e12 100644 --- a/internal/runtime/executor/codex_websockets_executor.go +++ b/internal/runtime/executor/codex_websockets_executor.go @@ -343,7 +343,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut } var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out)} + resp = cliproxyexecutor.Response{Payload: out} return resp, nil } } @@ -592,7 +592,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr line := encodeCodexWebsocketAsSSE(payload) chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, body, body, line, ¶m) for i := range chunks { - if !send(cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}) { + if !send(cliproxyexecutor.StreamChunk{Payload: chunks[i]}) { terminateReason = "context_done" terminateErr = ctx.Err() return diff --git a/internal/runtime/executor/gemini_cli_executor.go b/internal/runtime/executor/gemini_cli_executor.go index 1be245b7..7d2d2a9b 100644 --- a/internal/runtime/executor/gemini_cli_executor.go +++ b/internal/runtime/executor/gemini_cli_executor.go @@ -224,7 +224,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth reporter.publish(ctx, parseGeminiCLIUsage(data)) var param any out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -401,14 +401,14 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut if bytes.HasPrefix(line, dataTag) { segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m) for i := range segments { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])} + out <- cliproxyexecutor.StreamChunk{Payload: segments[i]} } } } segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, []byte("[DONE]"), ¶m) for i := range segments { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])} + out <- cliproxyexecutor.StreamChunk{Payload: segments[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) @@ -430,12 +430,12 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut var param any segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m) for i := range segments { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])} + out <- cliproxyexecutor.StreamChunk{Payload: segments[i]} } segments = sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, []byte("[DONE]"), ¶m) for i := range segments { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])} + out <- cliproxyexecutor.StreamChunk{Payload: segments[i]} } }(httpResp, append([]byte(nil), payload...), attemptModel) @@ -544,7 +544,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth. if resp.StatusCode >= 200 && resp.StatusCode < 300 { count := gjson.GetBytes(data, "totalTokens").Int() translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data) - return cliproxyexecutor.Response{Payload: []byte(translated), Headers: resp.Header.Clone()}, nil + return cliproxyexecutor.Response{Payload: translated, Headers: resp.Header.Clone()}, nil } lastStatus = resp.StatusCode lastBody = append([]byte(nil), data...) @@ -811,18 +811,18 @@ func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte { if !hasInlineData { emptyImageBase64ed, _ := util.CreateWhiteImageBase64(aspectRatioResult.String()) - emptyImagePart := `{"inlineData":{"mime_type":"image/png","data":""}}` - emptyImagePart, _ = sjson.Set(emptyImagePart, "inlineData.data", emptyImageBase64ed) - newPartsJson := `[]` - newPartsJson, _ = sjson.SetRaw(newPartsJson, "-1", `{"text": "Based on the following requirements, create an image within the uploaded picture. The new content *MUST* completely cover the entire area of the original picture, maintaining its exact proportions, and *NO* blank areas should appear."}`) - newPartsJson, _ = sjson.SetRaw(newPartsJson, "-1", emptyImagePart) + emptyImagePart := []byte(`{"inlineData":{"mime_type":"image/png","data":""}}`) + emptyImagePart, _ = sjson.SetBytes(emptyImagePart, "inlineData.data", emptyImageBase64ed) + newPartsJson := []byte(`[]`) + newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", []byte(`{"text": "Based on the following requirements, create an image within the uploaded picture. The new content *MUST* completely cover the entire area of the original picture, maintaining its exact proportions, and *NO* blank areas should appear."}`)) + newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", emptyImagePart) parts := contentArray[0].Get("parts").Array() for j := 0; j < len(parts); j++ { - newPartsJson, _ = sjson.SetRaw(newPartsJson, "-1", parts[j].Raw) + newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", []byte(parts[j].Raw)) } - rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents.0.parts", []byte(newPartsJson)) + rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents.0.parts", newPartsJson) rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.generationConfig.responseModalities", []byte(`["IMAGE", "TEXT"]`)) } } diff --git a/internal/runtime/executor/gemini_executor.go b/internal/runtime/executor/gemini_executor.go index 7c25b893..35b95da4 100644 --- a/internal/runtime/executor/gemini_executor.go +++ b/internal/runtime/executor/gemini_executor.go @@ -205,7 +205,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r reporter.publish(ctx, parseGeminiUsage(data)) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -321,12 +321,12 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} + out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} + out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) @@ -415,7 +415,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut count := gjson.GetBytes(data, "totalTokens").Int() translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data) - return cliproxyexecutor.Response{Payload: []byte(translated), Headers: resp.Header.Clone()}, nil + return cliproxyexecutor.Response{Payload: translated, Headers: resp.Header.Clone()}, nil } // Refresh refreshes the authentication credentials (no-op for Gemini API key). @@ -527,18 +527,18 @@ func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte { if !hasInlineData { emptyImageBase64ed, _ := util.CreateWhiteImageBase64(aspectRatioResult.String()) - emptyImagePart := `{"inlineData":{"mime_type":"image/png","data":""}}` - emptyImagePart, _ = sjson.Set(emptyImagePart, "inlineData.data", emptyImageBase64ed) - newPartsJson := `[]` - newPartsJson, _ = sjson.SetRaw(newPartsJson, "-1", `{"text": "Based on the following requirements, create an image within the uploaded picture. The new content *MUST* completely cover the entire area of the original picture, maintaining its exact proportions, and *NO* blank areas should appear."}`) - newPartsJson, _ = sjson.SetRaw(newPartsJson, "-1", emptyImagePart) + emptyImagePart := []byte(`{"inlineData":{"mime_type":"image/png","data":""}}`) + emptyImagePart, _ = sjson.SetBytes(emptyImagePart, "inlineData.data", emptyImageBase64ed) + newPartsJson := []byte(`[]`) + newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", []byte(`{"text": "Based on the following requirements, create an image within the uploaded picture. The new content *MUST* completely cover the entire area of the original picture, maintaining its exact proportions, and *NO* blank areas should appear."}`)) + newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", emptyImagePart) parts := contentArray[0].Get("parts").Array() for j := 0; j < len(parts); j++ { - newPartsJson, _ = sjson.SetRaw(newPartsJson, "-1", parts[j].Raw) + newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", []byte(parts[j].Raw)) } - rawJSON, _ = sjson.SetRawBytes(rawJSON, "contents.0.parts", []byte(newPartsJson)) + rawJSON, _ = sjson.SetRawBytes(rawJSON, "contents.0.parts", newPartsJson) rawJSON, _ = sjson.SetRawBytes(rawJSON, "generationConfig.responseModalities", []byte(`["IMAGE", "TEXT"]`)) } } diff --git a/internal/runtime/executor/gemini_vertex_executor.go b/internal/runtime/executor/gemini_vertex_executor.go index 84df56f9..13a2b65c 100644 --- a/internal/runtime/executor/gemini_vertex_executor.go +++ b/internal/runtime/executor/gemini_vertex_executor.go @@ -419,7 +419,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au to := sdktranslator.FromString("gemini") var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -524,7 +524,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip reporter.publish(ctx, parseGeminiUsage(data)) var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -636,12 +636,12 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} + out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} + out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) @@ -760,12 +760,12 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} + out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } } lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m) for i := range lines { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])} + out <- cliproxyexecutor.StreamChunk{Payload: lines[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) @@ -857,7 +857,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context appendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "totalTokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) - return cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()}, nil + return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil } // countTokensWithAPIKey handles token counting using API key credentials. @@ -941,7 +941,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth * appendAPIResponseChunk(ctx, e.cfg, data) count := gjson.GetBytes(data, "totalTokens").Int() out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data) - return cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()}, nil + return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil } // vertexCreds extracts project, location and raw service account JSON from auth metadata. diff --git a/internal/runtime/executor/iflow_executor.go b/internal/runtime/executor/iflow_executor.go index 65a0b8f8..cc5cc33d 100644 --- a/internal/runtime/executor/iflow_executor.go +++ b/internal/runtime/executor/iflow_executor.go @@ -169,7 +169,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re // Note: TranslateNonStream uses req.Model (original with suffix) to preserve // the original model name in the response for client compatibility. out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -281,7 +281,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } if errScan := scanner.Err(); errScan != nil { @@ -315,7 +315,7 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth usageJSON := buildOpenAIUsageJSON(count) translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON) - return cliproxyexecutor.Response{Payload: []byte(translated)}, nil + return cliproxyexecutor.Response{Payload: translated}, nil } // Refresh refreshes OAuth tokens or cookie-based API keys and updates the stored API key. diff --git a/internal/runtime/executor/kimi_executor.go b/internal/runtime/executor/kimi_executor.go index d5e3702f..e7052ee2 100644 --- a/internal/runtime/executor/kimi_executor.go +++ b/internal/runtime/executor/kimi_executor.go @@ -161,7 +161,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req // Note: TranslateNonStream uses req.Model (original with suffix) to preserve // the original model name in the response for client compatibility. out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -271,12 +271,12 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } doneChunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m) for i := range doneChunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(doneChunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) diff --git a/internal/runtime/executor/openai_compat_executor.go b/internal/runtime/executor/openai_compat_executor.go index 623c6620..3bb6e012 100644 --- a/internal/runtime/executor/openai_compat_executor.go +++ b/internal/runtime/executor/openai_compat_executor.go @@ -172,7 +172,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A // Translate response back to source format when needed var param any out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -290,7 +290,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy // Pass through translator; it yields one or more chunks for the target schema. chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(line), ¶m) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } if errScan := scanner.Err(); errScan != nil { @@ -330,7 +330,7 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau usageJSON := buildOpenAIUsageJSON(count) translatedUsage := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON) - return cliproxyexecutor.Response{Payload: []byte(translatedUsage)}, nil + return cliproxyexecutor.Response{Payload: translatedUsage}, nil } // Refresh is a no-op for API-key based compatibility providers. diff --git a/internal/runtime/executor/qwen_executor.go b/internal/runtime/executor/qwen_executor.go index e7957d29..ff19dcb5 100644 --- a/internal/runtime/executor/qwen_executor.go +++ b/internal/runtime/executor/qwen_executor.go @@ -305,7 +305,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req // Note: TranslateNonStream uses req.Model (original with suffix) to preserve // the original model name in the response for client compatibility. out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m) - resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()} + resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()} return resp, nil } @@ -421,12 +421,12 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut } chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m) for i := range chunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]} } } doneChunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m) for i := range doneChunks { - out <- cliproxyexecutor.StreamChunk{Payload: []byte(doneChunks[i])} + out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]} } if errScan := scanner.Err(); errScan != nil { recordAPIResponseError(ctx, e.cfg, errScan) @@ -461,7 +461,7 @@ func (e *QwenExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, usageJSON := buildOpenAIUsageJSON(count) translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON) - return cliproxyexecutor.Response{Payload: []byte(translated)}, nil + return cliproxyexecutor.Response{Payload: translated}, nil } func (e *QwenExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) { diff --git a/internal/translator/antigravity/claude/antigravity_claude_request.go b/internal/translator/antigravity/claude/antigravity_claude_request.go index bbe4498e..6b7f8c00 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_request.go +++ b/internal/translator/antigravity/claude/antigravity_claude_request.go @@ -40,33 +40,33 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ rawJSON := inputRawJSON // system instruction - systemInstructionJSON := "" + var systemInstructionJSON []byte hasSystemInstruction := false systemResult := gjson.GetBytes(rawJSON, "system") if systemResult.IsArray() { systemResults := systemResult.Array() - systemInstructionJSON = `{"role":"user","parts":[]}` + systemInstructionJSON = []byte(`{"role":"user","parts":[]}`) for i := 0; i < len(systemResults); i++ { systemPromptResult := systemResults[i] systemTypePromptResult := systemPromptResult.Get("type") if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" { systemPrompt := systemPromptResult.Get("text").String() - partJSON := `{}` + partJSON := []byte(`{}`) if systemPrompt != "" { - partJSON, _ = sjson.Set(partJSON, "text", systemPrompt) + partJSON, _ = sjson.SetBytes(partJSON, "text", systemPrompt) } - systemInstructionJSON, _ = sjson.SetRaw(systemInstructionJSON, "parts.-1", partJSON) + systemInstructionJSON, _ = sjson.SetRawBytes(systemInstructionJSON, "parts.-1", partJSON) hasSystemInstruction = true } } } else if systemResult.Type == gjson.String { - systemInstructionJSON = `{"role":"user","parts":[{"text":""}]}` - systemInstructionJSON, _ = sjson.Set(systemInstructionJSON, "parts.0.text", systemResult.String()) + systemInstructionJSON = []byte(`{"role":"user","parts":[{"text":""}]}`) + systemInstructionJSON, _ = sjson.SetBytes(systemInstructionJSON, "parts.0.text", systemResult.String()) hasSystemInstruction = true } // contents - contentsJSON := "[]" + contentsJSON := []byte(`[]`) hasContents := false // tool_use_id → tool_name lookup, populated incrementally during the main loop. @@ -88,8 +88,8 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ if role == "assistant" { role = "model" } - clientContentJSON := `{"role":"","parts":[]}` - clientContentJSON, _ = sjson.Set(clientContentJSON, "role", role) + clientContentJSON := []byte(`{"role":"","parts":[]}`) + clientContentJSON, _ = sjson.SetBytes(clientContentJSON, "role", role) contentsResult := messageResult.Get("content") if contentsResult.IsArray() { contentResults := contentsResult.Array() @@ -148,15 +148,15 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } // Valid signature, send as thought block - partJSON := `{}` - partJSON, _ = sjson.Set(partJSON, "thought", true) + partJSON := []byte(`{}`) + partJSON, _ = sjson.SetBytes(partJSON, "thought", true) if thinkingText != "" { - partJSON, _ = sjson.Set(partJSON, "text", thinkingText) + partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText) } if signature != "" { - partJSON, _ = sjson.Set(partJSON, "thoughtSignature", signature) + partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", signature) } - clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON) + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" { prompt := contentResult.Get("text").String() // Skip empty text parts to avoid Gemini API error: @@ -164,9 +164,9 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ if prompt == "" { continue } - partJSON := `{}` - partJSON, _ = sjson.Set(partJSON, "text", prompt) - clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON) + partJSON := []byte(`{}`) + partJSON, _ = sjson.SetBytes(partJSON, "text", prompt) + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" { // NOTE: Do NOT inject dummy thinking blocks here. // Antigravity API validates signatures, so dummy values are rejected. @@ -192,25 +192,25 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } if argsRaw != "" { - partJSON := `{}` + partJSON := []byte(`{}`) // Use skip_thought_signature_validator for tool calls without valid thinking signature // This is the approach used in opencode-google-antigravity-auth for Gemini // and also works for Claude through Antigravity API const skipSentinel = "skip_thought_signature_validator" if cache.HasValidSignature(modelName, currentMessageThinkingSignature) { - partJSON, _ = sjson.Set(partJSON, "thoughtSignature", currentMessageThinkingSignature) + partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", currentMessageThinkingSignature) } else { // No valid signature - use skip sentinel to bypass validation - partJSON, _ = sjson.Set(partJSON, "thoughtSignature", skipSentinel) + partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", skipSentinel) } if functionID != "" { - partJSON, _ = sjson.Set(partJSON, "functionCall.id", functionID) + partJSON, _ = sjson.SetBytes(partJSON, "functionCall.id", functionID) } - partJSON, _ = sjson.Set(partJSON, "functionCall.name", functionName) - partJSON, _ = sjson.SetRaw(partJSON, "functionCall.args", argsRaw) - clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON) + partJSON, _ = sjson.SetBytes(partJSON, "functionCall.name", functionName) + partJSON, _ = sjson.SetRawBytes(partJSON, "functionCall.args", []byte(argsRaw)) + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" { toolCallID := contentResult.Get("tool_use_id").String() @@ -231,108 +231,108 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ } functionResponseResult := contentResult.Get("content") - functionResponseJSON := `{}` - functionResponseJSON, _ = sjson.Set(functionResponseJSON, "id", toolCallID) - functionResponseJSON, _ = sjson.Set(functionResponseJSON, "name", funcName) + functionResponseJSON := []byte(`{}`) + functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "id", toolCallID) + functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "name", funcName) responseData := "" if functionResponseResult.Type == gjson.String { responseData = functionResponseResult.String() - functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", responseData) + functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", responseData) } else if functionResponseResult.IsArray() { frResults := functionResponseResult.Array() nonImageCount := 0 lastNonImageRaw := "" - filteredJSON := "[]" - imagePartsJSON := "[]" + filteredJSON := []byte(`[]`) + imagePartsJSON := []byte(`[]`) for _, fr := range frResults { if fr.Get("type").String() == "image" && fr.Get("source.type").String() == "base64" { - inlineDataJSON := `{}` + inlineDataJSON := []byte(`{}`) if mimeType := fr.Get("source.media_type").String(); mimeType != "" { - inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType) + inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "mimeType", mimeType) } if data := fr.Get("source.data").String(); data != "" { - inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data) + inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "data", data) } - imagePartJSON := `{}` - imagePartJSON, _ = sjson.SetRaw(imagePartJSON, "inlineData", inlineDataJSON) - imagePartsJSON, _ = sjson.SetRaw(imagePartsJSON, "-1", imagePartJSON) + imagePartJSON := []byte(`{}`) + imagePartJSON, _ = sjson.SetRawBytes(imagePartJSON, "inlineData", inlineDataJSON) + imagePartsJSON, _ = sjson.SetRawBytes(imagePartsJSON, "-1", imagePartJSON) continue } nonImageCount++ lastNonImageRaw = fr.Raw - filteredJSON, _ = sjson.SetRaw(filteredJSON, "-1", fr.Raw) + filteredJSON, _ = sjson.SetRawBytes(filteredJSON, "-1", []byte(fr.Raw)) } if nonImageCount == 1 { - functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", lastNonImageRaw) + functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", []byte(lastNonImageRaw)) } else if nonImageCount > 1 { - functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", filteredJSON) + functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", filteredJSON) } else { - functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "") + functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", "") } // Place image data inside functionResponse.parts as inlineData // instead of as sibling parts in the outer content, to avoid // base64 data bloating the text context. - if gjson.Get(imagePartsJSON, "#").Int() > 0 { - functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "parts", imagePartsJSON) + if gjson.GetBytes(imagePartsJSON, "#").Int() > 0 { + functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "parts", imagePartsJSON) } } else if functionResponseResult.IsObject() { if functionResponseResult.Get("type").String() == "image" && functionResponseResult.Get("source.type").String() == "base64" { - inlineDataJSON := `{}` + inlineDataJSON := []byte(`{}`) if mimeType := functionResponseResult.Get("source.media_type").String(); mimeType != "" { - inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType) + inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "mimeType", mimeType) } if data := functionResponseResult.Get("source.data").String(); data != "" { - inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data) + inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "data", data) } - imagePartJSON := `{}` - imagePartJSON, _ = sjson.SetRaw(imagePartJSON, "inlineData", inlineDataJSON) - imagePartsJSON := "[]" - imagePartsJSON, _ = sjson.SetRaw(imagePartsJSON, "-1", imagePartJSON) - functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "parts", imagePartsJSON) - functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "") + imagePartJSON := []byte(`{}`) + imagePartJSON, _ = sjson.SetRawBytes(imagePartJSON, "inlineData", inlineDataJSON) + imagePartsJSON := []byte(`[]`) + imagePartsJSON, _ = sjson.SetRawBytes(imagePartsJSON, "-1", imagePartJSON) + functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "parts", imagePartsJSON) + functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", "") } else { - functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw) + functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", []byte(functionResponseResult.Raw)) } } else if functionResponseResult.Raw != "" { - functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw) + functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", []byte(functionResponseResult.Raw)) } else { // Content field is missing entirely — .Raw is empty which // causes sjson.SetRaw to produce invalid JSON (e.g. "result":}). - functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "") + functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", "") } - partJSON := `{}` - partJSON, _ = sjson.SetRaw(partJSON, "functionResponse", functionResponseJSON) - clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON) + partJSON := []byte(`{}`) + partJSON, _ = sjson.SetRawBytes(partJSON, "functionResponse", functionResponseJSON) + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) } } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "image" { sourceResult := contentResult.Get("source") if sourceResult.Get("type").String() == "base64" { - inlineDataJSON := `{}` + inlineDataJSON := []byte(`{}`) if mimeType := sourceResult.Get("media_type").String(); mimeType != "" { - inlineDataJSON, _ = sjson.Set(inlineDataJSON, "mimeType", mimeType) + inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "mimeType", mimeType) } if data := sourceResult.Get("data").String(); data != "" { - inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data) + inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "data", data) } - partJSON := `{}` - partJSON, _ = sjson.SetRaw(partJSON, "inlineData", inlineDataJSON) - clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON) + partJSON := []byte(`{}`) + partJSON, _ = sjson.SetRawBytes(partJSON, "inlineData", inlineDataJSON) + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) } } } // Reorder parts for 'model' role to ensure thinking block is first if role == "model" { - partsResult := gjson.Get(clientContentJSON, "parts") + partsResult := gjson.GetBytes(clientContentJSON, "parts") if partsResult.IsArray() { parts := partsResult.Array() var thinkingParts []gjson.Result @@ -354,7 +354,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ for _, p := range otherParts { newParts = append(newParts, p.Value()) } - clientContentJSON, _ = sjson.Set(clientContentJSON, "parts", newParts) + clientContentJSON, _ = sjson.SetBytes(clientContentJSON, "parts", newParts) } } } @@ -362,33 +362,33 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ // Skip messages with empty parts array to avoid Gemini API error: // "required oneof field 'data' must have one initialized field" - partsCheck := gjson.Get(clientContentJSON, "parts") + partsCheck := gjson.GetBytes(clientContentJSON, "parts") if !partsCheck.IsArray() || len(partsCheck.Array()) == 0 { continue } - contentsJSON, _ = sjson.SetRaw(contentsJSON, "-1", clientContentJSON) + contentsJSON, _ = sjson.SetRawBytes(contentsJSON, "-1", clientContentJSON) hasContents = true } else if contentsResult.Type == gjson.String { prompt := contentsResult.String() - partJSON := `{}` + partJSON := []byte(`{}`) if prompt != "" { - partJSON, _ = sjson.Set(partJSON, "text", prompt) + partJSON, _ = sjson.SetBytes(partJSON, "text", prompt) } - clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON) - contentsJSON, _ = sjson.SetRaw(contentsJSON, "-1", clientContentJSON) + clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON) + contentsJSON, _ = sjson.SetRawBytes(contentsJSON, "-1", clientContentJSON) hasContents = true } } } // tools - toolsJSON := "" + var toolsJSON []byte toolDeclCount := 0 allowedToolKeys := []string{"name", "description", "behavior", "parameters", "parametersJsonSchema", "response", "responseJsonSchema"} toolsResult := gjson.GetBytes(rawJSON, "tools") if toolsResult.IsArray() { - toolsJSON = `[{"functionDeclarations":[]}]` + toolsJSON = []byte(`[{"functionDeclarations":[]}]`) toolsResults := toolsResult.Array() for i := 0; i < len(toolsResults); i++ { toolResult := toolsResults[i] @@ -396,23 +396,23 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { // Sanitize the input schema for Antigravity API compatibility inputSchema := util.CleanJSONSchemaForAntigravity(inputSchemaResult.Raw) - tool, _ := sjson.Delete(toolResult.Raw, "input_schema") - tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) - for toolKey := range gjson.Parse(tool).Map() { + tool, _ := sjson.DeleteBytes([]byte(toolResult.Raw), "input_schema") + tool, _ = sjson.SetRawBytes(tool, "parametersJsonSchema", []byte(inputSchema)) + for toolKey := range gjson.ParseBytes(tool).Map() { if util.InArray(allowedToolKeys, toolKey) { continue } - tool, _ = sjson.Delete(tool, toolKey) + tool, _ = sjson.DeleteBytes(tool, toolKey) } - toolsJSON, _ = sjson.SetRaw(toolsJSON, "0.functionDeclarations.-1", tool) + toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "0.functionDeclarations.-1", tool) toolDeclCount++ } } } // Build output Gemini CLI request JSON - out := `{"model":"","request":{"contents":[]}}` - out, _ = sjson.Set(out, "model", modelName) + out := []byte(`{"model":"","request":{"contents":[]}}`) + out, _ = sjson.SetBytes(out, "model", modelName) // Inject interleaved thinking hint when both tools and thinking are active hasTools := toolDeclCount > 0 @@ -426,27 +426,27 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ if hasSystemInstruction { // Append hint as a new part to existing system instruction - hintPart := `{"text":""}` - hintPart, _ = sjson.Set(hintPart, "text", interleavedHint) - systemInstructionJSON, _ = sjson.SetRaw(systemInstructionJSON, "parts.-1", hintPart) + hintPart := []byte(`{"text":""}`) + hintPart, _ = sjson.SetBytes(hintPart, "text", interleavedHint) + systemInstructionJSON, _ = sjson.SetRawBytes(systemInstructionJSON, "parts.-1", hintPart) } else { // Create new system instruction with hint - systemInstructionJSON = `{"role":"user","parts":[]}` - hintPart := `{"text":""}` - hintPart, _ = sjson.Set(hintPart, "text", interleavedHint) - systemInstructionJSON, _ = sjson.SetRaw(systemInstructionJSON, "parts.-1", hintPart) + systemInstructionJSON = []byte(`{"role":"user","parts":[]}`) + hintPart := []byte(`{"text":""}`) + hintPart, _ = sjson.SetBytes(hintPart, "text", interleavedHint) + systemInstructionJSON, _ = sjson.SetRawBytes(systemInstructionJSON, "parts.-1", hintPart) hasSystemInstruction = true } } if hasSystemInstruction { - out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstructionJSON) + out, _ = sjson.SetRawBytes(out, "request.systemInstruction", systemInstructionJSON) } if hasContents { - out, _ = sjson.SetRaw(out, "request.contents", contentsJSON) + out, _ = sjson.SetRawBytes(out, "request.contents", contentsJSON) } if toolDeclCount > 0 { - out, _ = sjson.SetRaw(out, "request.tools", toolsJSON) + out, _ = sjson.SetRawBytes(out, "request.tools", toolsJSON) } // tool_choice @@ -463,15 +463,15 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ switch toolChoiceType { case "auto": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "AUTO") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "AUTO") case "none": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "NONE") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "NONE") case "any": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY") case "tool": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY") if toolChoiceName != "" { - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) } } } @@ -482,8 +482,8 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ case "enabled": if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } case "adaptive", "auto": // For adaptive thinking: @@ -495,28 +495,27 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _ effort = strings.ToLower(strings.TrimSpace(v.String())) } if effort != "" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort) } else { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") } - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } } if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.temperature", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.temperature", v.Num) } if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.topP", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.topP", v.Num) } if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.topK", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.topK", v.Num) } if v := gjson.GetBytes(rawJSON, "max_tokens"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.maxOutputTokens", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.maxOutputTokens", v.Num) } - outBytes := []byte(out) - outBytes = common.AttachDefaultSafetySettings(outBytes, "request.safetySettings") + out = common.AttachDefaultSafetySettings(out, "request.safetySettings") - return outBytes + return out } diff --git a/internal/translator/antigravity/claude/antigravity_claude_response.go b/internal/translator/antigravity/claude/antigravity_claude_response.go index 893e4d07..6ffea7cb 100644 --- a/internal/translator/antigravity/claude/antigravity_claude_response.go +++ b/internal/translator/antigravity/claude/antigravity_claude_response.go @@ -15,6 +15,7 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/cache" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" log "github.com/sirupsen/logrus" @@ -63,8 +64,8 @@ var toolUseIDCounter uint64 // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Claude Code-compatible JSON response -func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of bytes, each containing a Claude Code-compatible SSE payload. +func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &Params{ HasFirstResponse: false, @@ -77,44 +78,44 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq params := (*param).(*Params) if bytes.Equal(rawJSON, []byte("[DONE]")) { - output := "" + output := make([]byte, 0, 256) // Only send final events if we have actually output content if params.HasContent { appendFinalEvents(params, &output, true) - return []string{ - output + "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n", - } + output = translatorcommon.AppendSSEEventString(output, "message_stop", `{"type":"message_stop"}`, 3) + return [][]byte{output} } - return []string{} + return [][]byte{} } - output := "" + output := make([]byte, 0, 1024) + appendEvent := func(event, payload string) { + output = translatorcommon.AppendSSEEventString(output, event, payload, 3) + } // Initialize the streaming session with a message_start event // This is only sent for the very first response chunk to establish the streaming session if !params.HasFirstResponse { - output = "event: message_start\n" - // Create the initial message structure with default values according to Claude Code API specification // This follows the Claude Code API specification for streaming message initialization - messageStartTemplate := `{"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-5-sonnet-20241022", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 0, "output_tokens": 0}}}` + messageStartTemplate := []byte(`{"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-5-sonnet-20241022", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 0, "output_tokens": 0}}}`) // Use cpaUsageMetadata within the message_start event for Claude. if promptTokenCount := gjson.GetBytes(rawJSON, "response.cpaUsageMetadata.promptTokenCount"); promptTokenCount.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.usage.input_tokens", promptTokenCount.Int()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.usage.input_tokens", promptTokenCount.Int()) } if candidatesTokenCount := gjson.GetBytes(rawJSON, "response.cpaUsageMetadata.candidatesTokenCount"); candidatesTokenCount.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.usage.output_tokens", candidatesTokenCount.Int()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.usage.output_tokens", candidatesTokenCount.Int()) } // Override default values with actual response metadata if available from the Gemini CLI response if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.model", modelVersionResult.String()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.model", modelVersionResult.String()) } if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.id", responseIDResult.String()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.id", responseIDResult.String()) } - output = output + fmt.Sprintf("data: %s\n\n\n", messageStartTemplate) + appendEvent("message_start", string(messageStartTemplate)) params.HasFirstResponse = true } @@ -144,15 +145,13 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq params.CurrentThinkingText.Reset() } - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex), "delta.signature", fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), thoughtSignature.String())) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex)), "delta.signature", fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), thoughtSignature.String())) + appendEvent("content_block_delta", string(data)) params.HasContent = true } else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state params.CurrentThinkingText.WriteString(partTextResult.String()) - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partTextResult.String()) + appendEvent("content_block_delta", string(data)) params.HasContent = true } else { // Transition from another state to thinking @@ -163,19 +162,14 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, params.ResponseIndex) // output = output + "\n\n\n" } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ } // Start a new thinking content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex), "delta.thinking", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex)) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partTextResult.String()) + appendEvent("content_block_delta", string(data)) params.ResponseType = 2 // Set state to thinking params.HasContent = true // Start accumulating thinking text for signature caching @@ -188,9 +182,8 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq // Process regular text content (user-visible output) // Continue existing text block if already in content state if params.ResponseType == 1 { - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", partTextResult.String()) + appendEvent("content_block_delta", string(data)) params.HasContent = true } else { // Transition from another state to text content @@ -201,19 +194,14 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, params.ResponseIndex) // output = output + "\n\n\n" } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ } if partTextResult.String() != "" { // Start a new text content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex)) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", partTextResult.String()) + appendEvent("content_block_delta", string(data)) params.ResponseType = 1 // Set state to content params.HasContent = true } @@ -229,9 +217,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq // Handle state transitions when switching to function calls // Close any existing function call block first if params.ResponseType == 3 { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ params.ResponseType = 0 } @@ -245,26 +231,21 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq // Close any other existing content block if params.ResponseType != 0 { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex)) params.ResponseIndex++ } // Start a new tool use content block // This creates the structure for a function call in Claude Code format - output = output + "event: content_block_start\n" - // Create the tool use block with unique ID and function details - data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, params.ResponseIndex) - data, _ = sjson.Set(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1)))) - data, _ = sjson.Set(data, "content_block.name", fcName) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data := []byte(fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, params.ResponseIndex)) + data, _ = sjson.SetBytes(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1)))) + data, _ = sjson.SetBytes(data, "content_block.name", fcName) + appendEvent("content_block_start", string(data)) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - output = output + "event: content_block_delta\n" - data, _ = sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, params.ResponseIndex), "delta.partial_json", fcArgsResult.Raw) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ = sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, params.ResponseIndex)), "delta.partial_json", fcArgsResult.Raw) + appendEvent("content_block_delta", string(data)) } params.ResponseType = 3 params.HasContent = true @@ -296,10 +277,10 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq appendFinalEvents(params, &output, false) } - return []string{output} + return [][]byte{output} } -func appendFinalEvents(params *Params, output *string, force bool) { +func appendFinalEvents(params *Params, output *[]byte, force bool) { if params.HasSentFinalEvents { return } @@ -314,9 +295,7 @@ func appendFinalEvents(params *Params, output *string, force bool) { } if params.ResponseType != 0 { - *output = *output + "event: content_block_stop\n" - *output = *output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex) - *output = *output + "\n\n\n" + *output = translatorcommon.AppendSSEEventString(*output, "content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex), 3) params.ResponseType = 0 } @@ -329,18 +308,16 @@ func appendFinalEvents(params *Params, output *string, force bool) { } } - *output = *output + "event: message_delta\n" - *output = *output + "data: " - delta := fmt.Sprintf(`{"type":"message_delta","delta":{"stop_reason":"%s","stop_sequence":null},"usage":{"input_tokens":%d,"output_tokens":%d}}`, stopReason, params.PromptTokenCount, usageOutputTokens) + delta := []byte(fmt.Sprintf(`{"type":"message_delta","delta":{"stop_reason":"%s","stop_sequence":null},"usage":{"input_tokens":%d,"output_tokens":%d}}`, stopReason, params.PromptTokenCount, usageOutputTokens)) // Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working) if params.CachedTokenCount > 0 { var err error - delta, err = sjson.Set(delta, "usage.cache_read_input_tokens", params.CachedTokenCount) + delta, err = sjson.SetBytes(delta, "usage.cache_read_input_tokens", params.CachedTokenCount) if err != nil { log.Warnf("antigravity claude response: failed to set cache_read_input_tokens: %v", err) } } - *output = *output + delta + "\n\n\n" + *output = translatorcommon.AppendSSEEventString(*output, "message_delta", string(delta), 3) params.HasSentFinalEvents = true } @@ -369,8 +346,8 @@ func resolveStopReason(params *Params) string { // - param: A pointer to a parameter object for the conversion. // // Returns: -// - string: A Claude-compatible JSON response. -func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Claude-compatible JSON response. +func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { _ = originalRequestRawJSON modelName := gjson.GetBytes(requestRawJSON, "model").String() @@ -388,15 +365,15 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or } } - responseJSON := `{"id":"","type":"message","role":"assistant","model":"","content":null,"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` - responseJSON, _ = sjson.Set(responseJSON, "id", root.Get("response.responseId").String()) - responseJSON, _ = sjson.Set(responseJSON, "model", root.Get("response.modelVersion").String()) - responseJSON, _ = sjson.Set(responseJSON, "usage.input_tokens", promptTokens) - responseJSON, _ = sjson.Set(responseJSON, "usage.output_tokens", outputTokens) + responseJSON := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":null,"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) + responseJSON, _ = sjson.SetBytes(responseJSON, "id", root.Get("response.responseId").String()) + responseJSON, _ = sjson.SetBytes(responseJSON, "model", root.Get("response.modelVersion").String()) + responseJSON, _ = sjson.SetBytes(responseJSON, "usage.input_tokens", promptTokens) + responseJSON, _ = sjson.SetBytes(responseJSON, "usage.output_tokens", outputTokens) // Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working) if cachedTokens > 0 { var err error - responseJSON, err = sjson.Set(responseJSON, "usage.cache_read_input_tokens", cachedTokens) + responseJSON, err = sjson.SetBytes(responseJSON, "usage.cache_read_input_tokens", cachedTokens) if err != nil { log.Warnf("antigravity claude response: failed to set cache_read_input_tokens: %v", err) } @@ -407,7 +384,7 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or if contentArrayInitialized { return } - responseJSON, _ = sjson.SetRaw(responseJSON, "content", "[]") + responseJSON, _ = sjson.SetRawBytes(responseJSON, "content", []byte("[]")) contentArrayInitialized = true } @@ -423,9 +400,9 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or return } ensureContentArray() - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", textBuilder.String()) - responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", textBuilder.String()) + responseJSON, _ = sjson.SetRawBytes(responseJSON, "content.-1", block) textBuilder.Reset() } @@ -434,12 +411,12 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or return } ensureContentArray() - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String()) if thinkingSignature != "" { - block, _ = sjson.Set(block, "signature", fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), thinkingSignature)) + block, _ = sjson.SetBytes(block, "signature", fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), thinkingSignature)) } - responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", block) + responseJSON, _ = sjson.SetRawBytes(responseJSON, "content.-1", block) thinkingBuilder.Reset() thinkingSignature = "" } @@ -475,16 +452,16 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or name := functionCall.Get("name").String() toolIDCounter++ - toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` - toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) - toolBlock, _ = sjson.Set(toolBlock, "name", name) + toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolBlock, _ = sjson.SetBytes(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) + toolBlock, _ = sjson.SetBytes(toolBlock, "name", name) if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) && args.IsObject() { - toolBlock, _ = sjson.SetRaw(toolBlock, "input", args.Raw) + toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(args.Raw)) } ensureContentArray() - responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", toolBlock) + responseJSON, _ = sjson.SetRawBytes(responseJSON, "content.-1", toolBlock) continue } } @@ -508,17 +485,17 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or } } } - responseJSON, _ = sjson.Set(responseJSON, "stop_reason", stopReason) + responseJSON, _ = sjson.SetBytes(responseJSON, "stop_reason", stopReason) if promptTokens == 0 && outputTokens == 0 { if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() { - responseJSON, _ = sjson.Delete(responseJSON, "usage") + responseJSON, _ = sjson.DeleteBytes(responseJSON, "usage") } } return responseJSON } -func ClaudeTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"input_tokens":%d}`, count) +func ClaudeTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.ClaudeInputTokensJSON(count) } diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_request.go b/internal/translator/antigravity/gemini/antigravity_gemini_request.go index e5ce0c31..3612c0fb 100644 --- a/internal/translator/antigravity/gemini/antigravity_gemini_request.go +++ b/internal/translator/antigravity/gemini/antigravity_gemini_request.go @@ -34,10 +34,10 @@ import ( // - []byte: The transformed request data in Gemini API format func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := inputRawJSON - template := "" - template = `{"project":"","request":{},"model":""}` - template, _ = sjson.SetRaw(template, "request", string(rawJSON)) - template, _ = sjson.Set(template, "model", modelName) + template := `{"project":"","request":{},"model":""}` + templateBytes, _ := sjson.SetRawBytes([]byte(template), "request", rawJSON) + templateBytes, _ = sjson.SetBytes(templateBytes, "model", modelName) + template = string(templateBytes) template, _ = sjson.Delete(template, "request.model") template, errFixCLIToolResponse := fixCLIToolResponse(template) @@ -47,7 +47,8 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ systemInstructionResult := gjson.Get(template, "request.system_instruction") if systemInstructionResult.Exists() { - template, _ = sjson.SetRaw(template, "request.systemInstruction", systemInstructionResult.Raw) + templateBytes, _ = sjson.SetRawBytes([]byte(template), "request.systemInstruction", []byte(systemInstructionResult.Raw)) + template = string(templateBytes) template, _ = sjson.Delete(template, "request.system_instruction") } rawJSON = []byte(template) @@ -149,7 +150,8 @@ func parseFunctionResponseRaw(response gjson.Result, fallbackName string) string raw := response.Raw name := response.Get("functionResponse.name").String() if strings.TrimSpace(name) == "" && fallbackName != "" { - raw, _ = sjson.Set(raw, "functionResponse.name", fallbackName) + updated, _ := sjson.SetBytes([]byte(raw), "functionResponse.name", fallbackName) + raw = string(updated) } return raw } @@ -157,27 +159,27 @@ func parseFunctionResponseRaw(response gjson.Result, fallbackName string) string log.Debugf("parse function response failed, using fallback") funcResp := response.Get("functionResponse") if funcResp.Exists() { - fr := `{"functionResponse":{"name":"","response":{"result":""}}}` + fr := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`) name := funcResp.Get("name").String() if strings.TrimSpace(name) == "" { name = fallbackName } - fr, _ = sjson.Set(fr, "functionResponse.name", name) - fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String()) + fr, _ = sjson.SetBytes(fr, "functionResponse.name", name) + fr, _ = sjson.SetBytes(fr, "functionResponse.response.result", funcResp.Get("response").String()) if id := funcResp.Get("id").String(); id != "" { - fr, _ = sjson.Set(fr, "functionResponse.id", id) + fr, _ = sjson.SetBytes(fr, "functionResponse.id", id) } - return fr + return string(fr) } useName := fallbackName if useName == "" { useName = "unknown" } - fr := `{"functionResponse":{"name":"","response":{"result":""}}}` - fr, _ = sjson.Set(fr, "functionResponse.name", useName) - fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String()) - return fr + fr := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`) + fr, _ = sjson.SetBytes(fr, "functionResponse.name", useName) + fr, _ = sjson.SetBytes(fr, "functionResponse.response.result", response.String()) + return string(fr) } // fixCLIToolResponse performs sophisticated tool response format conversion and grouping. @@ -204,7 +206,7 @@ func fixCLIToolResponse(input string) (string, error) { } // Initialize data structures for processing and grouping - contentsWrapper := `{"contents":[]}` + contentsWrapper := []byte(`{"contents":[]}`) var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses var collectedResponses []gjson.Result // Standalone responses to be matched @@ -237,16 +239,16 @@ func fixCLIToolResponse(input string) (string, error) { collectedResponses = collectedResponses[group.ResponsesNeeded:] // Create merged function response content - functionResponseContent := `{"parts":[],"role":"function"}` + functionResponseContent := []byte(`{"parts":[],"role":"function"}`) for ri, response := range groupResponses { partRaw := parseFunctionResponseRaw(response, group.CallNames[ri]) if partRaw != "" { - functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw) + functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts.-1", []byte(partRaw)) } } - if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) + if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 { + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent) } } @@ -269,7 +271,7 @@ func fixCLIToolResponse(input string) (string, error) { log.Warnf("failed to parse model content") return true } - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw)) // Create a new group for tracking responses group := &FunctionCallGroup{ @@ -283,7 +285,7 @@ func fixCLIToolResponse(input string) (string, error) { log.Warnf("failed to parse content") return true } - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw)) } } else { // Non-model content (user, etc.) @@ -291,7 +293,7 @@ func fixCLIToolResponse(input string) (string, error) { log.Warnf("failed to parse content") return true } - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw)) } return true @@ -303,23 +305,22 @@ func fixCLIToolResponse(input string) (string, error) { groupResponses := collectedResponses[:group.ResponsesNeeded] collectedResponses = collectedResponses[group.ResponsesNeeded:] - functionResponseContent := `{"parts":[],"role":"function"}` + functionResponseContent := []byte(`{"parts":[],"role":"function"}`) for ri, response := range groupResponses { partRaw := parseFunctionResponseRaw(response, group.CallNames[ri]) if partRaw != "" { - functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw) + functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts.-1", []byte(partRaw)) } } - if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) + if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 { + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent) } } } // Update the original JSON with the new contents - result := input - result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw) + result, _ := sjson.SetRawBytes([]byte(input), "request.contents", []byte(gjson.GetBytes(contentsWrapper, "contents").Raw)) - return result, nil + return string(result), nil } diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_response.go b/internal/translator/antigravity/gemini/antigravity_gemini_response.go index 874dc283..7b43c48d 100644 --- a/internal/translator/antigravity/gemini/antigravity_gemini_response.go +++ b/internal/translator/antigravity/gemini/antigravity_gemini_response.go @@ -8,8 +8,8 @@ package gemini import ( "bytes" "context" - "fmt" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -29,8 +29,8 @@ import ( // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - []string: The transformed request data in Gemini API format -func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string { +// - [][]byte: The transformed response data in Gemini API format. +func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte { if bytes.HasPrefix(rawJSON, []byte("data:")) { rawJSON = bytes.TrimSpace(rawJSON[5:]) } @@ -44,22 +44,22 @@ func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalR chunk = restoreUsageMetadata(chunk) } } else { - chunkTemplate := "[]" + chunkTemplate := []byte("[]") responseResult := gjson.ParseBytes(chunk) if responseResult.IsArray() { responseResultItems := responseResult.Array() for i := 0; i < len(responseResultItems); i++ { responseResultItem := responseResultItems[i] if responseResultItem.Get("response").Exists() { - chunkTemplate, _ = sjson.SetRaw(chunkTemplate, "-1", responseResultItem.Get("response").Raw) + chunkTemplate, _ = sjson.SetRawBytes(chunkTemplate, "-1", []byte(responseResultItem.Get("response").Raw)) } } } - chunk = []byte(chunkTemplate) + chunk = chunkTemplate } - return []string{string(chunk)} + return [][]byte{chunk} } - return []string{} + return [][]byte{} } // ConvertAntigravityResponseToGeminiNonStream converts a non-streaming Gemini CLI request to a non-streaming Gemini response. @@ -73,18 +73,18 @@ func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalR // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: A Gemini-compatible JSON response containing the response data -func ConvertAntigravityResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Gemini-compatible JSON response containing the response data. +func ConvertAntigravityResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { chunk := restoreUsageMetadata([]byte(responseResult.Raw)) - return string(chunk) + return chunk } - return string(rawJSON) + return rawJSON } -func GeminiTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } // restoreUsageMetadata renames cpaUsageMetadata back to usageMetadata. diff --git a/internal/translator/antigravity/gemini/antigravity_gemini_response_test.go b/internal/translator/antigravity/gemini/antigravity_gemini_response_test.go index 5f96012a..10bc722d 100644 --- a/internal/translator/antigravity/gemini/antigravity_gemini_response_test.go +++ b/internal/translator/antigravity/gemini/antigravity_gemini_response_test.go @@ -59,8 +59,8 @@ func TestConvertAntigravityResponseToGeminiNonStream(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := ConvertAntigravityResponseToGeminiNonStream(context.Background(), "", nil, nil, tt.input, nil) - if result != tt.expected { - t.Errorf("ConvertAntigravityResponseToGeminiNonStream() = %s, want %s", result, tt.expected) + if string(result) != tt.expected { + t.Errorf("ConvertAntigravityResponseToGeminiNonStream() = %s, want %s", string(result), tt.expected) } }) } @@ -87,8 +87,8 @@ func TestConvertAntigravityResponseToGeminiStream(t *testing.T) { if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } - if results[0] != tt.expected { - t.Errorf("ConvertAntigravityResponseToGemini() = %s, want %s", results[0], tt.expected) + if string(results[0]) != tt.expected { + t.Errorf("ConvertAntigravityResponseToGemini() = %s, want %s", string(results[0]), tt.expected) } }) } diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go index 7fb25b2a..6eff85f2 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_request.go @@ -354,31 +354,35 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _ if errRename != nil { log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename) var errSet error - fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object") + fnRawBytes, errSet := sjson.SetBytes([]byte(fnRaw), "parametersJsonSchema.type", "object") if errSet != nil { log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) continue } - fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) + fnRaw = string(fnRawBytes) + fnRawBytes, errSet = sjson.SetRawBytes([]byte(fnRaw), "parametersJsonSchema.properties", []byte(`{}`)) if errSet != nil { log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) continue } + fnRaw = string(fnRawBytes) } else { fnRaw = renamed } } else { var errSet error - fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object") + fnRawBytes, errSet := sjson.SetBytes([]byte(fnRaw), "parametersJsonSchema.type", "object") if errSet != nil { log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) continue } - fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) + fnRaw = string(fnRawBytes) + fnRawBytes, errSet = sjson.SetRawBytes([]byte(fnRaw), "parametersJsonSchema.properties", []byte(`{}`)) if errSet != nil { log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) continue } + fnRaw = string(fnRawBytes) } fnRaw, _ = sjson.Delete(fnRaw, "strict") if !hasFunction { diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go index 91bc0423..4f9445cd 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response.go @@ -44,8 +44,8 @@ var functionCallIDCounter uint64 // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing an OpenAI-compatible JSON response -func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of OpenAI-compatible JSON responses +func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &convertCliResponseToOpenAIChatParams{ UnixTimestamp: 0, @@ -54,15 +54,15 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq } if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } // Initialize the OpenAI SSE template. - template := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}` + template := []byte(`{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`) // Extract and set the model version. if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() { - template, _ = sjson.Set(template, "model", modelVersionResult.String()) + template, _ = sjson.SetBytes(template, "model", modelVersionResult.String()) } // Extract and set the creation timestamp. @@ -71,14 +71,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq if err == nil { (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp = t.Unix() } - template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) + template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) } else { - template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) + template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) } // Extract and set the response ID. if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() { - template, _ = sjson.Set(template, "id", responseIDResult.String()) + template, _ = sjson.SetBytes(template, "id", responseIDResult.String()) } // Cache the finish reason - do NOT set it in output yet (will be set on final chunk) @@ -90,21 +90,21 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() { cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int() if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) } if totalTokenCountResult := usageResult.Get("totalTokenCount"); totalTokenCountResult.Exists() { - template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int()) + template, _ = sjson.SetBytes(template, "usage.total_tokens", totalTokenCountResult.Int()) } promptTokenCount := usageResult.Get("promptTokenCount").Int() thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() - template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCount) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens", promptTokenCount) if thoughtsTokenCount > 0 { - template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) + template, _ = sjson.SetBytes(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) } // Include cached token count if present (indicates prompt caching is working) if cachedTokenCount > 0 { var err error - template, err = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) + template, err = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) if err != nil { log.Warnf("antigravity openai response: failed to set cached_tokens: %v", err) } @@ -141,33 +141,33 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq // Handle text content, distinguishing between regular content and reasoning/thoughts. if partResult.Get("thought").Bool() { - template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", textContent) + template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", textContent) } else { - template, _ = sjson.Set(template, "choices.0.delta.content", textContent) + template, _ = sjson.SetBytes(template, "choices.0.delta.content", textContent) } - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") } else if functionCallResult.Exists() { // Handle function call content. (*param).(*convertCliResponseToOpenAIChatParams).SawToolCall = true // Persist across chunks - toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls") + toolCallsResult := gjson.GetBytes(template, "choices.0.delta.tool_calls") functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++ if toolCallsResult.Exists() && toolCallsResult.IsArray() { functionCallIndex = len(toolCallsResult.Array()) } else { - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) } - functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` + functionCallTemplate := []byte(`{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}`) fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", fcArgsResult.Raw) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.arguments", fcArgsResult.Raw) } - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallTemplate) } else if inlineDataResult.Exists() { data := inlineDataResult.Get("data").String() if data == "" { @@ -181,16 +181,16 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq mimeType = "image/png" } imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) - imagesResult := gjson.Get(template, "choices.0.delta.images") + imagesResult := gjson.GetBytes(template, "choices.0.delta.images") if !imagesResult.Exists() || !imagesResult.IsArray() { - template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images", []byte(`[]`)) } - imageIndex := len(gjson.Get(template, "choices.0.delta.images").Array()) - imagePayload := `{"type":"image_url","image_url":{"url":""}}` - imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex) - imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload) + imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array()) + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload) } } } @@ -212,11 +212,11 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq } else { finishReason = "stop" } - template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason) - template, _ = sjson.Set(template, "choices.0.native_finish_reason", strings.ToLower(upstreamFinishReason)) + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", strings.ToLower(upstreamFinishReason)) } - return []string{template} + return [][]byte{template} } // ConvertAntigravityResponseToOpenAINonStream converts a non-streaming Gemini CLI response to a non-streaming OpenAI response. @@ -231,11 +231,11 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq // - param: A pointer to a parameter object for the conversion // // Returns: -// - string: An OpenAI-compatible JSON response containing all message content and metadata -func ConvertAntigravityResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +// - []byte: An OpenAI-compatible JSON response containing all message content and metadata +func ConvertAntigravityResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { return ConvertGeminiResponseToOpenAINonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, []byte(responseResult.Raw), param) } - return "" + return []byte{} } diff --git a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response_test.go b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response_test.go index eea1ad52..bd2eb891 100644 --- a/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response_test.go +++ b/internal/translator/antigravity/openai/chat-completions/antigravity_openai_response_test.go @@ -19,7 +19,7 @@ func TestFinishReasonToolCallsNotOverwritten(t *testing.T) { if len(result1) != 1 { t.Fatalf("Expected 1 result from chunk1, got %d", len(result1)) } - fr1 := gjson.Get(result1[0], "choices.0.finish_reason") + fr1 := gjson.GetBytes(result1[0], "choices.0.finish_reason") if fr1.Exists() && fr1.String() != "" && fr1.Type.String() != "Null" { t.Errorf("Expected finish_reason to be null in chunk1, got: %v", fr1.String()) } @@ -33,13 +33,13 @@ func TestFinishReasonToolCallsNotOverwritten(t *testing.T) { if len(result2) != 1 { t.Fatalf("Expected 1 result from chunk2, got %d", len(result2)) } - fr2 := gjson.Get(result2[0], "choices.0.finish_reason").String() + fr2 := gjson.GetBytes(result2[0], "choices.0.finish_reason").String() if fr2 != "tool_calls" { t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr2) } // Verify native_finish_reason is lowercase upstream value - nfr2 := gjson.Get(result2[0], "choices.0.native_finish_reason").String() + nfr2 := gjson.GetBytes(result2[0], "choices.0.native_finish_reason").String() if nfr2 != "stop" { t.Errorf("Expected native_finish_reason 'stop', got: %s", nfr2) } @@ -58,7 +58,7 @@ func TestFinishReasonStopForNormalText(t *testing.T) { result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m) // Verify finish_reason is "stop" (no tool calls were made) - fr := gjson.Get(result2[0], "choices.0.finish_reason").String() + fr := gjson.GetBytes(result2[0], "choices.0.finish_reason").String() if fr != "stop" { t.Errorf("Expected finish_reason 'stop', got: %s", fr) } @@ -77,7 +77,7 @@ func TestFinishReasonMaxTokens(t *testing.T) { result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m) // Verify finish_reason is "max_tokens" - fr := gjson.Get(result2[0], "choices.0.finish_reason").String() + fr := gjson.GetBytes(result2[0], "choices.0.finish_reason").String() if fr != "max_tokens" { t.Errorf("Expected finish_reason 'max_tokens', got: %s", fr) } @@ -96,7 +96,7 @@ func TestToolCallTakesPriorityOverMaxTokens(t *testing.T) { result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m) // Verify finish_reason is "tool_calls" (takes priority over max_tokens) - fr := gjson.Get(result2[0], "choices.0.finish_reason").String() + fr := gjson.GetBytes(result2[0], "choices.0.finish_reason").String() if fr != "tool_calls" { t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr) } @@ -111,7 +111,7 @@ func TestNoFinishReasonOnIntermediateChunks(t *testing.T) { result1 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m) // Verify no finish_reason on intermediate chunk - fr1 := gjson.Get(result1[0], "choices.0.finish_reason") + fr1 := gjson.GetBytes(result1[0], "choices.0.finish_reason") if fr1.Exists() && fr1.String() != "" && fr1.Type.String() != "Null" { t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr1) } @@ -121,7 +121,7 @@ func TestNoFinishReasonOnIntermediateChunks(t *testing.T) { result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m) // Verify no finish_reason on intermediate chunk - fr2 := gjson.Get(result2[0], "choices.0.finish_reason") + fr2 := gjson.GetBytes(result2[0], "choices.0.finish_reason") if fr2.Exists() && fr2.String() != "" && fr2.Type.String() != "Null" { t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr2) } diff --git a/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go b/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go index 7c416c1f..a087e0bd 100644 --- a/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go +++ b/internal/translator/antigravity/openai/responses/antigravity_openai-responses_response.go @@ -7,7 +7,7 @@ import ( "github.com/tidwall/gjson" ) -func ConvertAntigravityResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func ConvertAntigravityResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { rawJSON = []byte(responseResult.Raw) @@ -15,7 +15,7 @@ func ConvertAntigravityResponseToOpenAIResponses(ctx context.Context, modelName return ConvertGeminiResponseToOpenAIResponses(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) } -func ConvertAntigravityResponseToOpenAIResponsesNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +func ConvertAntigravityResponseToOpenAIResponsesNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { rawJSON = []byte(responseResult.Raw) diff --git a/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go b/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go index bc072b30..62e2650f 100644 --- a/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go +++ b/internal/translator/claude/gemini-cli/claude_gemini-cli_response.go @@ -8,7 +8,7 @@ import ( "context" . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini" - "github.com/tidwall/sjson" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" ) // ConvertClaudeResponseToGeminiCLI converts Claude Code streaming response format to Gemini CLI format. @@ -23,15 +23,13 @@ import ( // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object -func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Gemini-compatible JSON responses wrapped in a response object +func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { outputs := ConvertClaudeResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) // Wrap each converted response in a "response" object to match Gemini CLI API structure - newOutputs := make([]string, 0) + newOutputs := make([][]byte, 0, len(outputs)) for i := 0; i < len(outputs); i++ { - json := `{"response": {}}` - output, _ := sjson.SetRaw(json, "response", outputs[i]) - newOutputs = append(newOutputs, output) + newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i])) } return newOutputs } @@ -47,15 +45,13 @@ func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, ori // - param: A pointer to a parameter object for the conversion // // Returns: -// - string: A Gemini-compatible JSON response wrapped in a response object -func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { - strJSON := ConvertClaudeResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) +// - []byte: A Gemini-compatible JSON response wrapped in a response object +func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { + out := ConvertClaudeResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) // Wrap the converted response in a "response" object to match Gemini CLI API structure - json := `{"response": {}}` - strJSON, _ = sjson.SetRaw(json, "response", strJSON) - return strJSON + return translatorcommon.WrapGeminiCLIResponse(out) } -func GeminiCLITokenCount(ctx context.Context, count int64) string { +func GeminiCLITokenCount(ctx context.Context, count int64) []byte { return GeminiTokenCount(ctx, count) } diff --git a/internal/translator/claude/gemini/claude_gemini_request.go b/internal/translator/claude/gemini/claude_gemini_request.go index a8d97b9d..d2a215e7 100644 --- a/internal/translator/claude/gemini/claude_gemini_request.go +++ b/internal/translator/claude/gemini/claude_gemini_request.go @@ -63,7 +63,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session) // Base Claude message payload - out := fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID) + out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID)) root := gjson.ParseBytes(rawJSON) @@ -87,20 +87,20 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream var pendingToolIDs []string // Model mapping to specify which Claude Code model to use - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Generation config extraction from Gemini format if genConfig := root.Get("generationConfig"); genConfig.Exists() { // Max output tokens configuration if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() { - out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } // Temperature setting for controlling response randomness if temp := genConfig.Get("temperature"); temp.Exists() { - out, _ = sjson.Set(out, "temperature", temp.Float()) + out, _ = sjson.SetBytes(out, "temperature", temp.Float()) } else if topP := genConfig.Get("topP"); topP.Exists() { // Top P setting for nucleus sampling (filtered out if temperature is set) - out, _ = sjson.Set(out, "top_p", topP.Float()) + out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Stop sequences configuration for custom termination conditions if stopSeqs := genConfig.Get("stopSequences"); stopSeqs.Exists() && stopSeqs.IsArray() { @@ -110,7 +110,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream return true }) if len(stopSequences) > 0 { - out, _ = sjson.Set(out, "stop_sequences", stopSequences) + out, _ = sjson.SetBytes(out, "stop_sequences", stopSequences) } } // Include thoughts configuration for reasoning process visibility @@ -132,30 +132,30 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream switch level { case "": case "none": - out, _ = sjson.Set(out, "thinking.type", "disabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Delete(out, "output_config.effort") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.DeleteBytes(out, "output_config.effort") default: if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok { level = mapped } - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Set(out, "output_config.effort", level) + out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "output_config.effort", level) } } else { switch level { case "": case "none": - out, _ = sjson.Set(out, "thinking.type", "disabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") case "auto": - out, _ = sjson.Set(out, "thinking.type", "enabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") default: if budget, ok := thinking.ConvertLevelToBudget(level); ok { - out, _ = sjson.Set(out, "thinking.type", "enabled") - out, _ = sjson.Set(out, "thinking.budget_tokens", budget) + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } @@ -169,37 +169,37 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream if supportsAdaptive { switch budget { case 0: - out, _ = sjson.Set(out, "thinking.type", "disabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Delete(out, "output_config.effort") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.DeleteBytes(out, "output_config.effort") default: level, ok := thinking.ConvertBudgetToLevel(budget) if ok { if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM { level = mapped } - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Set(out, "output_config.effort", level) + out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "output_config.effort", level) } } } else { switch budget { case 0: - out, _ = sjson.Set(out, "thinking.type", "disabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") case -1: - out, _ = sjson.Set(out, "thinking.type", "enabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") default: - out, _ = sjson.Set(out, "thinking.type", "enabled") - out, _ = sjson.Set(out, "thinking.budget_tokens", budget) + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } else if includeThoughts := thinkingConfig.Get("includeThoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True { - out, _ = sjson.Set(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") } else if includeThoughts := thinkingConfig.Get("include_thoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True { - out, _ = sjson.Set(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") } } } @@ -220,9 +220,9 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream }) if systemText.Len() > 0 { // Create system message in Claude Code format - systemMessage := `{"role":"user","content":[{"type":"text","text":""}]}` - systemMessage, _ = sjson.Set(systemMessage, "content.0.text", systemText.String()) - out, _ = sjson.SetRaw(out, "messages.-1", systemMessage) + systemMessage := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`) + systemMessage, _ = sjson.SetBytes(systemMessage, "content.0.text", systemText.String()) + out, _ = sjson.SetRawBytes(out, "messages.-1", systemMessage) } } } @@ -245,42 +245,42 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream } // Create message structure in Claude Code format - msg := `{"role":"","content":[]}` - msg, _ = sjson.Set(msg, "role", role) + msg := []byte(`{"role":"","content":[]}`) + msg, _ = sjson.SetBytes(msg, "role", role) if parts := content.Get("parts"); parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { // Text content conversion if text := part.Get("text"); text.Exists() { - textContent := `{"type":"text","text":""}` - textContent, _ = sjson.Set(textContent, "text", text.String()) - msg, _ = sjson.SetRaw(msg, "content.-1", textContent) + textContent := []byte(`{"type":"text","text":""}`) + textContent, _ = sjson.SetBytes(textContent, "text", text.String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", textContent) return true } // Function call (from model/assistant) conversion to tool use if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" { - toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` + toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) // Generate a unique tool ID and enqueue it for later matching // with the corresponding functionResponse toolID := genToolCallID() pendingToolIDs = append(pendingToolIDs, toolID) - toolUse, _ = sjson.Set(toolUse, "id", toolID) + toolUse, _ = sjson.SetBytes(toolUse, "id", toolID) if name := fc.Get("name"); name.Exists() { - toolUse, _ = sjson.Set(toolUse, "name", name.String()) + toolUse, _ = sjson.SetBytes(toolUse, "name", name.String()) } if args := fc.Get("args"); args.Exists() && args.IsObject() { - toolUse, _ = sjson.SetRaw(toolUse, "input", args.Raw) + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(args.Raw)) } - msg, _ = sjson.SetRaw(msg, "content.-1", toolUse) + msg, _ = sjson.SetRawBytes(msg, "content.-1", toolUse) return true } // Function response (from user) conversion to tool result if fr := part.Get("functionResponse"); fr.Exists() { - toolResult := `{"type":"tool_result","tool_use_id":"","content":""}` + toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`) // Attach the oldest queued tool_id to pair the response // with its call. If the queue is empty, generate a new id. @@ -293,41 +293,41 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream // Fallback: generate new ID if no pending tool_use found toolID = genToolCallID() } - toolResult, _ = sjson.Set(toolResult, "tool_use_id", toolID) + toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", toolID) // Extract result content from the function response if result := fr.Get("response.result"); result.Exists() { - toolResult, _ = sjson.Set(toolResult, "content", result.String()) + toolResult, _ = sjson.SetBytes(toolResult, "content", result.String()) } else if response := fr.Get("response"); response.Exists() { - toolResult, _ = sjson.Set(toolResult, "content", response.Raw) + toolResult, _ = sjson.SetBytes(toolResult, "content", response.Raw) } - msg, _ = sjson.SetRaw(msg, "content.-1", toolResult) + msg, _ = sjson.SetRawBytes(msg, "content.-1", toolResult) return true } // Image content (inline_data) conversion to Claude Code format if inlineData := part.Get("inline_data"); inlineData.Exists() { - imageContent := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}` + imageContent := []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`) if mimeType := inlineData.Get("mime_type"); mimeType.Exists() { - imageContent, _ = sjson.Set(imageContent, "source.media_type", mimeType.String()) + imageContent, _ = sjson.SetBytes(imageContent, "source.media_type", mimeType.String()) } if data := inlineData.Get("data"); data.Exists() { - imageContent, _ = sjson.Set(imageContent, "source.data", data.String()) + imageContent, _ = sjson.SetBytes(imageContent, "source.data", data.String()) } - msg, _ = sjson.SetRaw(msg, "content.-1", imageContent) + msg, _ = sjson.SetRawBytes(msg, "content.-1", imageContent) return true } // File data conversion to text content with file info if fileData := part.Get("file_data"); fileData.Exists() { // For file data, we'll convert to text content with file info - textContent := `{"type":"text","text":""}` + textContent := []byte(`{"type":"text","text":""}`) fileInfo := "File: " + fileData.Get("file_uri").String() if mimeType := fileData.Get("mime_type"); mimeType.Exists() { fileInfo += " (Type: " + mimeType.String() + ")" } - textContent, _ = sjson.Set(textContent, "text", fileInfo) - msg, _ = sjson.SetRaw(msg, "content.-1", textContent) + textContent, _ = sjson.SetBytes(textContent, "text", fileInfo) + msg, _ = sjson.SetRawBytes(msg, "content.-1", textContent) return true } @@ -336,8 +336,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream } // Only add message if it has content - if contentArray := gjson.Get(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 { - out, _ = sjson.SetRaw(out, "messages.-1", msg) + if contentArray := gjson.GetBytes(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } return true @@ -351,29 +351,29 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream tools.ForEach(func(_, tool gjson.Result) bool { if funcDecls := tool.Get("functionDeclarations"); funcDecls.Exists() && funcDecls.IsArray() { funcDecls.ForEach(func(_, funcDecl gjson.Result) bool { - anthropicTool := `{"name":"","description":"","input_schema":{}}` + anthropicTool := []byte(`{"name":"","description":"","input_schema":{}}`) if name := funcDecl.Get("name"); name.Exists() { - anthropicTool, _ = sjson.Set(anthropicTool, "name", name.String()) + anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", name.String()) } if desc := funcDecl.Get("description"); desc.Exists() { - anthropicTool, _ = sjson.Set(anthropicTool, "description", desc.String()) + anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", desc.String()) } if params := funcDecl.Get("parameters"); params.Exists() { // Clean up the parameters schema for Claude Code compatibility - cleaned := params.Raw - cleaned, _ = sjson.Set(cleaned, "additionalProperties", false) - cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#") - anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned) + cleaned := []byte(params.Raw) + cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false) + cleaned, _ = sjson.SetBytes(cleaned, "$schema", "http://json-schema.org/draft-07/schema#") + anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned) } else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() { // Clean up the parameters schema for Claude Code compatibility - cleaned := params.Raw - cleaned, _ = sjson.Set(cleaned, "additionalProperties", false) - cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#") - anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned) + cleaned := []byte(params.Raw) + cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false) + cleaned, _ = sjson.SetBytes(cleaned, "$schema", "http://json-schema.org/draft-07/schema#") + anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned) } - anthropicTools = append(anthropicTools, gjson.Parse(anthropicTool).Value()) + anthropicTools = append(anthropicTools, gjson.ParseBytes(anthropicTool).Value()) return true }) } @@ -381,7 +381,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream }) if len(anthropicTools) > 0 { - out, _ = sjson.Set(out, "tools", anthropicTools) + out, _ = sjson.SetBytes(out, "tools", anthropicTools) } } @@ -391,27 +391,27 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream if mode := funcCalling.Get("mode"); mode.Exists() { switch mode.String() { case "AUTO": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "NONE": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"none"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"none"}`)) case "ANY": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } } } } // Stream setting configuration - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // Convert tool parameter types to lowercase for Claude Code compatibility var pathsToLower []string - toolsResult := gjson.Get(out, "tools") + toolsResult := gjson.GetBytes(out, "tools") util.Walk(toolsResult, "", "type", &pathsToLower) for _, p := range pathsToLower { fullPath := fmt.Sprintf("tools.%s", p) - out, _ = sjson.Set(out, fullPath, strings.ToLower(gjson.Get(out, fullPath).String())) + out, _ = sjson.SetBytes(out, fullPath, strings.ToLower(gjson.GetBytes(out, fullPath).String())) } - return []byte(out) + return out } diff --git a/internal/translator/claude/gemini/claude_gemini_response.go b/internal/translator/claude/gemini/claude_gemini_response.go index c38f8ae7..846c2605 100644 --- a/internal/translator/claude/gemini/claude_gemini_response.go +++ b/internal/translator/claude/gemini/claude_gemini_response.go @@ -9,10 +9,10 @@ import ( "bufio" "bytes" "context" - "fmt" "strings" "time" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -30,7 +30,7 @@ type ConvertAnthropicResponseToGeminiParams struct { Model string CreatedAt int64 ResponseID string - LastStorageOutput string + LastStorageOutput []byte IsStreaming bool // Streaming state for tool_use assembly @@ -52,8 +52,8 @@ type ConvertAnthropicResponseToGeminiParams struct { // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Gemini-compatible JSON response -func ConvertClaudeResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Gemini-compatible JSON responses +func ConvertClaudeResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertAnthropicResponseToGeminiParams{ Model: modelName, @@ -63,7 +63,7 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original } if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) @@ -71,24 +71,24 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original eventType := root.Get("type").String() // Base Gemini response template with default values - template := `{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}` + template := []byte(`{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`) // Set model version if (*param).(*ConvertAnthropicResponseToGeminiParams).Model != "" { // Map Claude model names back to Gemini model names - template, _ = sjson.Set(template, "modelVersion", (*param).(*ConvertAnthropicResponseToGeminiParams).Model) + template, _ = sjson.SetBytes(template, "modelVersion", (*param).(*ConvertAnthropicResponseToGeminiParams).Model) } // Set response ID and creation time if (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID != "" { - template, _ = sjson.Set(template, "responseId", (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID) + template, _ = sjson.SetBytes(template, "responseId", (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID) } // Set creation time to current time if not provided if (*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt == 0 { (*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt = time.Now().Unix() } - template, _ = sjson.Set(template, "createTime", time.Unix((*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano)) + template, _ = sjson.SetBytes(template, "createTime", time.Unix((*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano)) switch eventType { case "message_start": @@ -97,7 +97,7 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID = message.Get("id").String() (*param).(*ConvertAnthropicResponseToGeminiParams).Model = message.Get("model").String() } - return []string{} + return [][]byte{} case "content_block_start": // Start of a content block - record tool_use name by index for functionCall assembly @@ -112,7 +112,7 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original } } } - return []string{} + return [][]byte{} case "content_block_delta": // Handle content delta (text, thinking, or tool use arguments) @@ -123,16 +123,16 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original case "text_delta": // Regular text content delta for normal response text if text := delta.Get("text"); text.Exists() && text.String() != "" { - textPart := `{"text":""}` - textPart, _ = sjson.Set(textPart, "text", text.String()) - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", textPart) + textPart := []byte(`{"text":""}`) + textPart, _ = sjson.SetBytes(textPart, "text", text.String()) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", textPart) } case "thinking_delta": // Thinking/reasoning content delta for models with reasoning capabilities if text := delta.Get("thinking"); text.Exists() && text.String() != "" { - thinkingPart := `{"thought":true,"text":""}` - thinkingPart, _ = sjson.Set(thinkingPart, "text", text.String()) - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", thinkingPart) + thinkingPart := []byte(`{"thought":true,"text":""}`) + thinkingPart, _ = sjson.SetBytes(thinkingPart, "text", text.String()) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", thinkingPart) } case "input_json_delta": // Tool use input delta - accumulate partial_json by index for later assembly at content_block_stop @@ -149,10 +149,10 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original if pj := delta.Get("partial_json"); pj.Exists() { b.WriteString(pj.String()) } - return []string{} + return [][]byte{} } } - return []string{template} + return [][]byte{template} case "content_block_stop": // End of content block - finalize tool calls if any @@ -170,16 +170,16 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original } } if name != "" || argsTrim != "" { - functionCall := `{"functionCall":{"name":"","args":{}}}` + functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`) if name != "" { - functionCall, _ = sjson.Set(functionCall, "functionCall.name", name) + functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", name) } if argsTrim != "" { - functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsTrim) + functionCall, _ = sjson.SetRawBytes(functionCall, "functionCall.args", []byte(argsTrim)) } - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", functionCall) - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") - (*param).(*ConvertAnthropicResponseToGeminiParams).LastStorageOutput = template + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", functionCall) + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") + (*param).(*ConvertAnthropicResponseToGeminiParams).LastStorageOutput = append([]byte(nil), template...) // cleanup used state for this index if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs != nil { delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs, idx) @@ -187,9 +187,9 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames != nil { delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames, idx) } - return []string{template} + return [][]byte{template} } - return []string{} + return [][]byte{} case "message_delta": // Handle message-level changes (like stop reason and usage information) @@ -197,15 +197,15 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original if stopReason := delta.Get("stop_reason"); stopReason.Exists() { switch stopReason.String() { case "end_turn": - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") case "tool_use": - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") case "max_tokens": - template, _ = sjson.Set(template, "candidates.0.finishReason", "MAX_TOKENS") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "MAX_TOKENS") case "stop_sequence": - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") default: - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") } } } @@ -216,35 +216,35 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original outputTokens := usage.Get("output_tokens").Int() // Set basic usage metadata according to Gemini API specification - template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens) - template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens) - template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", inputTokens+outputTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", inputTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", outputTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", inputTokens+outputTokens) // Add cache-related token counts if present (Claude Code API cache fields) if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() { - template, _ = sjson.Set(template, "usageMetadata.cachedContentTokenCount", cacheCreationTokens.Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.cachedContentTokenCount", cacheCreationTokens.Int()) } if cacheReadTokens := usage.Get("cache_read_input_tokens"); cacheReadTokens.Exists() { // Add cache read tokens to cached content count existingCacheTokens := usage.Get("cache_creation_input_tokens").Int() totalCacheTokens := existingCacheTokens + cacheReadTokens.Int() - template, _ = sjson.Set(template, "usageMetadata.cachedContentTokenCount", totalCacheTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.cachedContentTokenCount", totalCacheTokens) } // Add thinking tokens if present (for models with reasoning capabilities) if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() { - template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", thinkingTokens.Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", thinkingTokens.Int()) } // Set traffic type (required by Gemini API) - template, _ = sjson.Set(template, "usageMetadata.trafficType", "PROVISIONED_THROUGHPUT") + template, _ = sjson.SetBytes(template, "usageMetadata.trafficType", "PROVISIONED_THROUGHPUT") } - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") - return []string{template} + return [][]byte{template} case "message_stop": // Final message with usage information - no additional output needed - return []string{} + return [][]byte{} case "error": // Handle error responses and convert to Gemini error format errorMsg := root.Get("error.message").String() @@ -253,13 +253,13 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original } // Create error response in Gemini format - errorResponse := `{"error":{"code":400,"message":"","status":"INVALID_ARGUMENT"}}` - errorResponse, _ = sjson.Set(errorResponse, "error.message", errorMsg) - return []string{errorResponse} + errorResponse := []byte(`{"error":{"code":400,"message":"","status":"INVALID_ARGUMENT"}}`) + errorResponse, _ = sjson.SetBytes(errorResponse, "error.message", errorMsg) + return [][]byte{errorResponse} default: // Unknown event type, return empty response - return []string{} + return [][]byte{} } } @@ -275,13 +275,13 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: A Gemini-compatible JSON response containing all message content and metadata -func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Gemini-compatible JSON response containing all message content and metadata +func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { // Base Gemini response template for non-streaming with default values - template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}` + template := []byte(`{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`) // Set model version - template, _ = sjson.Set(template, "modelVersion", modelName) + template, _ = sjson.SetBytes(template, "modelVersion", modelName) streamingEvents := make([][]byte, 0) @@ -304,15 +304,15 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, Model: modelName, CreatedAt: 0, ResponseID: "", - LastStorageOutput: "", + LastStorageOutput: nil, IsStreaming: false, ToolUseNames: nil, ToolUseArgs: nil, } // Process each streaming event and collect parts - var allParts []string - var finalUsageJSON string + var allParts [][]byte + var finalUsageJSON []byte var responseID string var createdAt int64 @@ -360,15 +360,15 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, case "text_delta": // Process regular text content if text := delta.Get("text"); text.Exists() && text.String() != "" { - partJSON := `{"text":""}` - partJSON, _ = sjson.Set(partJSON, "text", text.String()) + partJSON := []byte(`{"text":""}`) + partJSON, _ = sjson.SetBytes(partJSON, "text", text.String()) allParts = append(allParts, partJSON) } case "thinking_delta": // Process reasoning/thinking content if text := delta.Get("thinking"); text.Exists() && text.String() != "" { - partJSON := `{"thought":true,"text":""}` - partJSON, _ = sjson.Set(partJSON, "text", text.String()) + partJSON := []byte(`{"thought":true,"text":""}`) + partJSON, _ = sjson.SetBytes(partJSON, "text", text.String()) allParts = append(allParts, partJSON) } case "input_json_delta": @@ -402,12 +402,12 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, } } if name != "" || argsTrim != "" { - functionCallJSON := `{"functionCall":{"name":"","args":{}}}` + functionCallJSON := []byte(`{"functionCall":{"name":"","args":{}}}`) if name != "" { - functionCallJSON, _ = sjson.Set(functionCallJSON, "functionCall.name", name) + functionCallJSON, _ = sjson.SetBytes(functionCallJSON, "functionCall.name", name) } if argsTrim != "" { - functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim) + functionCallJSON, _ = sjson.SetRawBytes(functionCallJSON, "functionCall.args", []byte(argsTrim)) } allParts = append(allParts, functionCallJSON) // cleanup used state for this index @@ -422,35 +422,35 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, case "message_delta": // Extract final usage information using sjson for token counts and metadata if usage := root.Get("usage"); usage.Exists() { - usageJSON := `{}` + usageJSON := []byte(`{}`) // Basic token counts for prompt and completion inputTokens := usage.Get("input_tokens").Int() outputTokens := usage.Get("output_tokens").Int() // Set basic usage metadata according to Gemini API specification - usageJSON, _ = sjson.Set(usageJSON, "promptTokenCount", inputTokens) - usageJSON, _ = sjson.Set(usageJSON, "candidatesTokenCount", outputTokens) - usageJSON, _ = sjson.Set(usageJSON, "totalTokenCount", inputTokens+outputTokens) + usageJSON, _ = sjson.SetBytes(usageJSON, "promptTokenCount", inputTokens) + usageJSON, _ = sjson.SetBytes(usageJSON, "candidatesTokenCount", outputTokens) + usageJSON, _ = sjson.SetBytes(usageJSON, "totalTokenCount", inputTokens+outputTokens) // Add cache-related token counts if present (Claude Code API cache fields) if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() { - usageJSON, _ = sjson.Set(usageJSON, "cachedContentTokenCount", cacheCreationTokens.Int()) + usageJSON, _ = sjson.SetBytes(usageJSON, "cachedContentTokenCount", cacheCreationTokens.Int()) } if cacheReadTokens := usage.Get("cache_read_input_tokens"); cacheReadTokens.Exists() { // Add cache read tokens to cached content count existingCacheTokens := usage.Get("cache_creation_input_tokens").Int() totalCacheTokens := existingCacheTokens + cacheReadTokens.Int() - usageJSON, _ = sjson.Set(usageJSON, "cachedContentTokenCount", totalCacheTokens) + usageJSON, _ = sjson.SetBytes(usageJSON, "cachedContentTokenCount", totalCacheTokens) } // Add thinking tokens if present (for models with reasoning capabilities) if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() { - usageJSON, _ = sjson.Set(usageJSON, "thoughtsTokenCount", thinkingTokens.Int()) + usageJSON, _ = sjson.SetBytes(usageJSON, "thoughtsTokenCount", thinkingTokens.Int()) } // Set traffic type (required by Gemini API) - usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT") + usageJSON, _ = sjson.SetBytes(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT") finalUsageJSON = usageJSON } @@ -459,10 +459,10 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, // Set response metadata if responseID != "" { - template, _ = sjson.Set(template, "responseId", responseID) + template, _ = sjson.SetBytes(template, "responseId", responseID) } if createdAt > 0 { - template, _ = sjson.Set(template, "createTime", time.Unix(createdAt, 0).Format(time.RFC3339Nano)) + template, _ = sjson.SetBytes(template, "createTime", time.Unix(createdAt, 0).Format(time.RFC3339Nano)) } // Consolidate consecutive text parts and thinking parts for cleaner output @@ -470,35 +470,35 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, // Set the consolidated parts array if len(consolidatedParts) > 0 { - partsJSON := "[]" + partsJSON := []byte(`[]`) for _, partJSON := range consolidatedParts { - partsJSON, _ = sjson.SetRaw(partsJSON, "-1", partJSON) + partsJSON, _ = sjson.SetRawBytes(partsJSON, "-1", partJSON) } - template, _ = sjson.SetRaw(template, "candidates.0.content.parts", partsJSON) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts", partsJSON) } // Set usage metadata - if finalUsageJSON != "" { - template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON) + if len(finalUsageJSON) > 0 { + template, _ = sjson.SetRawBytes(template, "usageMetadata", finalUsageJSON) } return template } -func GeminiTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } // consolidateParts merges consecutive text parts and thinking parts to create a cleaner response. // This function processes the parts array to combine adjacent text elements and thinking elements // into single consolidated parts, which results in a more readable and efficient response structure. // Tool calls and other non-text parts are preserved as separate elements. -func consolidateParts(parts []string) []string { +func consolidateParts(parts [][]byte) [][]byte { if len(parts) == 0 { return parts } - var consolidated []string + var consolidated [][]byte var currentTextPart strings.Builder var currentThoughtPart strings.Builder var hasText, hasThought bool @@ -506,8 +506,8 @@ func consolidateParts(parts []string) []string { flushText := func() { // Flush accumulated text content to the consolidated parts array if hasText && currentTextPart.Len() > 0 { - textPartJSON := `{"text":""}` - textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String()) + textPartJSON := []byte(`{"text":""}`) + textPartJSON, _ = sjson.SetBytes(textPartJSON, "text", currentTextPart.String()) consolidated = append(consolidated, textPartJSON) currentTextPart.Reset() hasText = false @@ -517,8 +517,8 @@ func consolidateParts(parts []string) []string { flushThought := func() { // Flush accumulated thinking content to the consolidated parts array if hasThought && currentThoughtPart.Len() > 0 { - thoughtPartJSON := `{"thought":true,"text":""}` - thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String()) + thoughtPartJSON := []byte(`{"thought":true,"text":""}`) + thoughtPartJSON, _ = sjson.SetBytes(thoughtPartJSON, "text", currentThoughtPart.String()) consolidated = append(consolidated, thoughtPartJSON) currentThoughtPart.Reset() hasThought = false @@ -526,7 +526,7 @@ func consolidateParts(parts []string) []string { } for _, partJSON := range parts { - part := gjson.Parse(partJSON) + part := gjson.ParseBytes(partJSON) if !part.Exists() || !part.IsObject() { // Flush any pending parts and add this non-text part flushText() diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_request.go b/internal/translator/claude/openai/chat-completions/claude_openai_request.go index ef01bb94..112e286d 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_request.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_request.go @@ -61,7 +61,7 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session) // Base Claude Code API template with default max_tokens value - out := fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID) + out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID)) root := gjson.ParseBytes(rawJSON) @@ -79,20 +79,20 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream if supportsAdaptive { switch effort { case "none": - out, _ = sjson.Set(out, "thinking.type", "disabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Delete(out, "output_config.effort") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.DeleteBytes(out, "output_config.effort") case "auto": - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Delete(out, "output_config.effort") + out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.DeleteBytes(out, "output_config.effort") default: if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { effort = mapped } - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Set(out, "output_config.effort", effort) + out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "output_config.effort", effort) } } else { // Legacy/manual thinking (budget_tokens). @@ -100,13 +100,13 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream if ok { switch budget { case 0: - out, _ = sjson.Set(out, "thinking.type", "disabled") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") case -1: - out, _ = sjson.Set(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") default: if budget > 0 { - out, _ = sjson.Set(out, "thinking.type", "enabled") - out, _ = sjson.Set(out, "thinking.budget_tokens", budget) + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } @@ -128,19 +128,19 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream } // Model mapping to specify which Claude Code model to use - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Max tokens configuration with fallback to default value if maxTokens := root.Get("max_tokens"); maxTokens.Exists() { - out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } // Temperature setting for controlling response randomness if temp := root.Get("temperature"); temp.Exists() { - out, _ = sjson.Set(out, "temperature", temp.Float()) + out, _ = sjson.SetBytes(out, "temperature", temp.Float()) } else if topP := root.Get("top_p"); topP.Exists() { // Top P setting for nucleus sampling (filtered out if temperature is set) - out, _ = sjson.Set(out, "top_p", topP.Float()) + out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Stop sequences configuration for custom termination conditions @@ -152,15 +152,15 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream return true }) if len(stopSequences) > 0 { - out, _ = sjson.Set(out, "stop_sequences", stopSequences) + out, _ = sjson.SetBytes(out, "stop_sequences", stopSequences) } } else { - out, _ = sjson.Set(out, "stop_sequences", []string{stop.String()}) + out, _ = sjson.SetBytes(out, "stop_sequences", []string{stop.String()}) } } // Stream configuration to enable or disable streaming responses - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // Process messages and transform them to Claude Code format if messages := root.Get("messages"); messages.Exists() && messages.IsArray() { @@ -173,39 +173,39 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream switch role { case "system": if systemMessageIndex == -1 { - systemMsg := `{"role":"user","content":[]}` - out, _ = sjson.SetRaw(out, "messages.-1", systemMsg) + systemMsg := []byte(`{"role":"user","content":[]}`) + out, _ = sjson.SetRawBytes(out, "messages.-1", systemMsg) systemMessageIndex = messageIndex messageIndex++ } if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" { - textPart := `{"type":"text","text":""}` - textPart, _ = sjson.Set(textPart, "text", contentResult.String()) - out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart) + textPart := []byte(`{"type":"text","text":""}`) + textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String()) + out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart) } else if contentResult.Exists() && contentResult.IsArray() { contentResult.ForEach(func(_, part gjson.Result) bool { if part.Get("type").String() == "text" { - textPart := `{"type":"text","text":""}` - textPart, _ = sjson.Set(textPart, "text", part.Get("text").String()) - out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart) + textPart := []byte(`{"type":"text","text":""}`) + textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String()) + out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart) } return true }) } case "user", "assistant": - msg := `{"role":"","content":[]}` - msg, _ = sjson.Set(msg, "role", role) + msg := []byte(`{"role":"","content":[]}`) + msg, _ = sjson.SetBytes(msg, "role", role) // Handle content based on its type (string or array) if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" { - part := `{"type":"text","text":""}` - part, _ = sjson.Set(part, "text", contentResult.String()) - msg, _ = sjson.SetRaw(msg, "content.-1", part) + part := []byte(`{"type":"text","text":""}`) + part, _ = sjson.SetBytes(part, "text", contentResult.String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) } else if contentResult.Exists() && contentResult.IsArray() { contentResult.ForEach(func(_, part gjson.Result) bool { claudePart := convertOpenAIContentPartToClaudePart(part) if claudePart != "" { - msg, _ = sjson.SetRaw(msg, "content.-1", claudePart) + msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(claudePart)) } return true }) @@ -221,9 +221,9 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream } function := toolCall.Get("function") - toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` - toolUse, _ = sjson.Set(toolUse, "id", toolCallID) - toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String()) + toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolUse, _ = sjson.SetBytes(toolUse, "id", toolCallID) + toolUse, _ = sjson.SetBytes(toolUse, "name", function.Get("name").String()) // Parse arguments for the tool call if args := function.Get("arguments"); args.Exists() { @@ -231,24 +231,24 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { - toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw) + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw)) } else { - toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}")) } } else { - toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}")) } } else { - toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}")) } - msg, _ = sjson.SetRaw(msg, "content.-1", toolUse) + msg, _ = sjson.SetRawBytes(msg, "content.-1", toolUse) } return true }) } - out, _ = sjson.SetRaw(out, "messages.-1", msg) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) messageIndex++ case "tool": @@ -256,15 +256,15 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream toolCallID := message.Get("tool_call_id").String() toolContentResult := message.Get("content") - msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}` - msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID) + msg := []byte(`{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`) + msg, _ = sjson.SetBytes(msg, "content.0.tool_use_id", toolCallID) toolResultContent, toolResultContentRaw := convertOpenAIToolResultContent(toolContentResult) if toolResultContentRaw { - msg, _ = sjson.SetRaw(msg, "content.0.content", toolResultContent) + msg, _ = sjson.SetRawBytes(msg, "content.0.content", []byte(toolResultContent)) } else { - msg, _ = sjson.Set(msg, "content.0.content", toolResultContent) + msg, _ = sjson.SetBytes(msg, "content.0.content", toolResultContent) } - out, _ = sjson.SetRaw(out, "messages.-1", msg) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) messageIndex++ } return true @@ -277,25 +277,25 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream tools.ForEach(func(_, tool gjson.Result) bool { if tool.Get("type").String() == "function" { function := tool.Get("function") - anthropicTool := `{"name":"","description":""}` - anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String()) - anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String()) + anthropicTool := []byte(`{"name":"","description":""}`) + anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", function.Get("name").String()) + anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", function.Get("description").String()) // Convert parameters schema for the tool if parameters := function.Get("parameters"); parameters.Exists() { - anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw) + anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", []byte(parameters.Raw)) } else if parameters := function.Get("parametersJsonSchema"); parameters.Exists() { - anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", parameters.Raw) + anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", []byte(parameters.Raw)) } - out, _ = sjson.SetRaw(out, "tools.-1", anthropicTool) + out, _ = sjson.SetRawBytes(out, "tools.-1", anthropicTool) hasAnthropicTools = true } return true }) if !hasAnthropicTools { - out, _ = sjson.Delete(out, "tools") + out, _ = sjson.DeleteBytes(out, "tools") } } @@ -308,31 +308,31 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream case "none": // Don't set tool_choice, Claude Code will not use tools case "auto": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "required": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } case gjson.JSON: // Specific tool choice mapping if toolChoice.Get("type").String() == "function" { functionName := toolChoice.Get("function.name").String() - toolChoiceJSON := `{"type":"tool","name":""}` - toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName) - out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON) + toolChoiceJSON := []byte(`{"type":"tool","name":""}`) + toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "name", functionName) + out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON) } default: } } - return []byte(out) + return out } func convertOpenAIContentPartToClaudePart(part gjson.Result) string { switch part.Get("type").String() { case "text": - textPart := `{"type":"text","text":""}` - textPart, _ = sjson.Set(textPart, "text", part.Get("text").String()) - return textPart + textPart := []byte(`{"type":"text","text":""}`) + textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String()) + return string(textPart) case "image_url": return convertOpenAIImageURLToClaudePart(part.Get("image_url.url").String()) @@ -345,10 +345,10 @@ func convertOpenAIContentPartToClaudePart(part gjson.Result) string { if semicolonIdx != -1 && commaIdx != -1 && commaIdx > semicolonIdx { mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:") data := fileData[commaIdx+1:] - docPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}` - docPart, _ = sjson.Set(docPart, "source.media_type", mediaType) - docPart, _ = sjson.Set(docPart, "source.data", data) - return docPart + docPart := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`) + docPart, _ = sjson.SetBytes(docPart, "source.media_type", mediaType) + docPart, _ = sjson.SetBytes(docPart, "source.data", data) + return string(docPart) } } } @@ -373,15 +373,15 @@ func convertOpenAIImageURLToClaudePart(imageURL string) string { mediaType = "application/octet-stream" } - imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}` - imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType) - imagePart, _ = sjson.Set(imagePart, "source.data", parts[1]) - return imagePart + imagePart := []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`) + imagePart, _ = sjson.SetBytes(imagePart, "source.media_type", mediaType) + imagePart, _ = sjson.SetBytes(imagePart, "source.data", parts[1]) + return string(imagePart) } - imagePart := `{"type":"image","source":{"type":"url","url":""}}` - imagePart, _ = sjson.Set(imagePart, "source.url", imageURL) - return imagePart + imagePart := []byte(`{"type":"image","source":{"type":"url","url":""}}`) + imagePart, _ = sjson.SetBytes(imagePart, "source.url", imageURL) + return string(imagePart) } func convertOpenAIToolResultContent(content gjson.Result) (string, bool) { @@ -394,28 +394,28 @@ func convertOpenAIToolResultContent(content gjson.Result) (string, bool) { } if content.IsArray() { - claudeContent := "[]" + claudeContent := []byte("[]") partCount := 0 content.ForEach(func(_, part gjson.Result) bool { if part.Type == gjson.String { - textPart := `{"type":"text","text":""}` - textPart, _ = sjson.Set(textPart, "text", part.String()) - claudeContent, _ = sjson.SetRaw(claudeContent, "-1", textPart) + textPart := []byte(`{"type":"text","text":""}`) + textPart, _ = sjson.SetBytes(textPart, "text", part.String()) + claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", textPart) partCount++ return true } claudePart := convertOpenAIContentPartToClaudePart(part) if claudePart != "" { - claudeContent, _ = sjson.SetRaw(claudeContent, "-1", claudePart) + claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", []byte(claudePart)) partCount++ } return true }) if partCount > 0 || len(content.Array()) == 0 { - return claudeContent, true + return string(claudeContent), true } return content.Raw, false @@ -424,9 +424,9 @@ func convertOpenAIToolResultContent(content gjson.Result) (string, bool) { if content.IsObject() { claudePart := convertOpenAIContentPartToClaudePart(content) if claudePart != "" { - claudeContent := "[]" - claudeContent, _ = sjson.SetRaw(claudeContent, "-1", claudePart) - return claudeContent, true + claudeContent := []byte("[]") + claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", []byte(claudePart)) + return string(claudeContent), true } return content.Raw, false } diff --git a/internal/translator/claude/openai/chat-completions/claude_openai_response.go b/internal/translator/claude/openai/chat-completions/claude_openai_response.go index 0ddfeaec..18d79a8f 100644 --- a/internal/translator/claude/openai/chat-completions/claude_openai_response.go +++ b/internal/translator/claude/openai/chat-completions/claude_openai_response.go @@ -48,8 +48,8 @@ type ToolCallAccumulator struct { // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing an OpenAI-compatible JSON response -func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of OpenAI-compatible JSON responses +func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertAnthropicResponseToOpenAIParams{ CreatedAt: 0, @@ -59,7 +59,7 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original } if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) @@ -67,19 +67,19 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original eventType := root.Get("type").String() // Base OpenAI streaming response template - template := `{"id":"","object":"chat.completion.chunk","created":0,"model":"","choices":[{"index":0,"delta":{},"finish_reason":null}]}` + template := []byte(`{"id":"","object":"chat.completion.chunk","created":0,"model":"","choices":[{"index":0,"delta":{},"finish_reason":null}]}`) // Set model if modelName != "" { - template, _ = sjson.Set(template, "model", modelName) + template, _ = sjson.SetBytes(template, "model", modelName) } // Set response ID and creation time if (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID != "" { - template, _ = sjson.Set(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID) + template, _ = sjson.SetBytes(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID) } if (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt > 0 { - template, _ = sjson.Set(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt) + template, _ = sjson.SetBytes(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt) } switch eventType { @@ -89,19 +89,19 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID = message.Get("id").String() (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt = time.Now().Unix() - template, _ = sjson.Set(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID) - template, _ = sjson.Set(template, "model", modelName) - template, _ = sjson.Set(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt) + template, _ = sjson.SetBytes(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID) + template, _ = sjson.SetBytes(template, "model", modelName) + template, _ = sjson.SetBytes(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt) // Set initial role to assistant for the response - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") // Initialize tool calls accumulator for tracking tool call progress if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator == nil { (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator = make(map[int]*ToolCallAccumulator) } } - return []string{template} + return [][]byte{template} case "content_block_start": // Start of a content block (text, tool use, or reasoning) @@ -124,10 +124,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original } // Don't output anything yet - wait for complete tool call - return []string{} + return [][]byte{} } } - return []string{} + return [][]byte{} case "content_block_delta": // Handle content delta (text, tool use arguments, or reasoning content) @@ -139,13 +139,13 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original case "text_delta": // Text content delta - send incremental text updates if text := delta.Get("text"); text.Exists() { - template, _ = sjson.Set(template, "choices.0.delta.content", text.String()) + template, _ = sjson.SetBytes(template, "choices.0.delta.content", text.String()) hasContent = true } case "thinking_delta": // Accumulate reasoning/thinking content if thinking := delta.Get("thinking"); thinking.Exists() { - template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", thinking.String()) + template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", thinking.String()) hasContent = true } case "input_json_delta": @@ -159,13 +159,13 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original } } // Don't output anything yet - wait for complete tool call - return []string{} + return [][]byte{} } } if hasContent { - return []string{template} + return [][]byte{template} } else { - return []string{} + return [][]byte{} } case "content_block_stop": @@ -178,26 +178,26 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original if arguments == "" { arguments = "{}" } - template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index) - template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID) - template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function") - template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name) - template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.function.arguments", arguments) + template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.index", index) + template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.id", accumulator.ID) + template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.type", "function") + template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.function.name", accumulator.Name) + template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.function.arguments", arguments) // Clean up the accumulator for this index delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index) - return []string{template} + return [][]byte{template} } } - return []string{} + return [][]byte{} case "message_delta": // Handle message-level changes including stop reason and usage if delta := root.Get("delta"); delta.Exists() { if stopReason := delta.Get("stop_reason"); stopReason.Exists() { (*param).(*ConvertAnthropicResponseToOpenAIParams).FinishReason = mapAnthropicStopReasonToOpenAI(stopReason.String()) - template, _ = sjson.Set(template, "choices.0.finish_reason", (*param).(*ConvertAnthropicResponseToOpenAIParams).FinishReason) + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", (*param).(*ConvertAnthropicResponseToOpenAIParams).FinishReason) } } @@ -207,34 +207,34 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original outputTokens := usage.Get("output_tokens").Int() cacheReadInputTokens := usage.Get("cache_read_input_tokens").Int() cacheCreationInputTokens := usage.Get("cache_creation_input_tokens").Int() - template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens) - template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens) - template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens) - template, _ = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens) + template, _ = sjson.SetBytes(template, "usage.completion_tokens", outputTokens) + template, _ = sjson.SetBytes(template, "usage.total_tokens", inputTokens+outputTokens) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens) } - return []string{template} + return [][]byte{template} case "message_stop": // Final message event - no additional output needed - return []string{} + return [][]byte{} case "ping": // Ping events for keeping connection alive - no output needed - return []string{} + return [][]byte{} case "error": // Error event - format and return error response if errorData := root.Get("error"); errorData.Exists() { - errorJSON := `{"error":{"message":"","type":""}}` - errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String()) - errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String()) - return []string{errorJSON} + errorJSON := []byte(`{"error":{"message":"","type":""}}`) + errorJSON, _ = sjson.SetBytes(errorJSON, "error.message", errorData.Get("message").String()) + errorJSON, _ = sjson.SetBytes(errorJSON, "error.type", errorData.Get("type").String()) + return [][]byte{errorJSON} } - return []string{} + return [][]byte{} default: // Unknown event type - ignore - return []string{} + return [][]byte{} } } @@ -266,8 +266,8 @@ func mapAnthropicStopReasonToOpenAI(anthropicReason string) string { // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: An OpenAI-compatible JSON response containing all message content and metadata -func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: An OpenAI-compatible JSON response containing all message content and metadata +func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { chunks := make([][]byte, 0) lines := bytes.Split(rawJSON, []byte("\n")) @@ -279,7 +279,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina } // Base OpenAI non-streaming response template - out := `{"id":"","object":"chat.completion","created":0,"model":"","choices":[{"index":0,"message":{"role":"assistant","content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}` + out := []byte(`{"id":"","object":"chat.completion","created":0,"model":"","choices":[{"index":0,"message":{"role":"assistant","content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}`) var messageID string var model string @@ -366,28 +366,28 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina outputTokens := usage.Get("output_tokens").Int() cacheReadInputTokens := usage.Get("cache_read_input_tokens").Int() cacheCreationInputTokens := usage.Get("cache_creation_input_tokens").Int() - out, _ = sjson.Set(out, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens) - out, _ = sjson.Set(out, "usage.completion_tokens", outputTokens) - out, _ = sjson.Set(out, "usage.total_tokens", inputTokens+outputTokens) - out, _ = sjson.Set(out, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens) + out, _ = sjson.SetBytes(out, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens) + out, _ = sjson.SetBytes(out, "usage.completion_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.total_tokens", inputTokens+outputTokens) + out, _ = sjson.SetBytes(out, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens) } } } // Set basic response fields including message ID, creation time, and model - out, _ = sjson.Set(out, "id", messageID) - out, _ = sjson.Set(out, "created", createdAt) - out, _ = sjson.Set(out, "model", model) + out, _ = sjson.SetBytes(out, "id", messageID) + out, _ = sjson.SetBytes(out, "created", createdAt) + out, _ = sjson.SetBytes(out, "model", model) // Set message content by combining all text parts messageContent := strings.Join(contentParts, "") - out, _ = sjson.Set(out, "choices.0.message.content", messageContent) + out, _ = sjson.SetBytes(out, "choices.0.message.content", messageContent) // Add reasoning content if available (following OpenAI reasoning format) if len(reasoningParts) > 0 { reasoningContent := strings.Join(reasoningParts, "") // Add reasoning as a separate field in the message - out, _ = sjson.Set(out, "choices.0.message.reasoning", reasoningContent) + out, _ = sjson.SetBytes(out, "choices.0.message.reasoning", reasoningContent) } // Set tool calls if any were accumulated during processing @@ -413,19 +413,19 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount) argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount) - out, _ = sjson.Set(out, idPath, accumulator.ID) - out, _ = sjson.Set(out, typePath, "function") - out, _ = sjson.Set(out, namePath, accumulator.Name) - out, _ = sjson.Set(out, argumentsPath, arguments) + out, _ = sjson.SetBytes(out, idPath, accumulator.ID) + out, _ = sjson.SetBytes(out, typePath, "function") + out, _ = sjson.SetBytes(out, namePath, accumulator.Name) + out, _ = sjson.SetBytes(out, argumentsPath, arguments) toolCallsCount++ } if toolCallsCount > 0 { - out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls") + out, _ = sjson.SetBytes(out, "choices.0.finish_reason", "tool_calls") } else { - out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason)) + out, _ = sjson.SetBytes(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason)) } } else { - out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason)) + out, _ = sjson.SetBytes(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason)) } return out diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_request.go b/internal/translator/claude/openai/responses/claude_openai-responses_request.go index cb550b09..514129ca 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_request.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_request.go @@ -49,7 +49,7 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session) // Base Claude message payload - out := fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID) + out := []byte(fmt.Sprintf(`{"model":"","max_tokens":32000,"messages":[],"metadata":{"user_id":"%s"}}`, userID)) root := gjson.ParseBytes(rawJSON) @@ -67,20 +67,20 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte if supportsAdaptive { switch effort { case "none": - out, _ = sjson.Set(out, "thinking.type", "disabled") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Delete(out, "output_config.effort") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.DeleteBytes(out, "output_config.effort") case "auto": - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Delete(out, "output_config.effort") + out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.DeleteBytes(out, "output_config.effort") default: if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok { effort = mapped } - out, _ = sjson.Set(out, "thinking.type", "adaptive") - out, _ = sjson.Delete(out, "thinking.budget_tokens") - out, _ = sjson.Set(out, "output_config.effort", effort) + out, _ = sjson.SetBytes(out, "thinking.type", "adaptive") + out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens") + out, _ = sjson.SetBytes(out, "output_config.effort", effort) } } else { // Legacy/manual thinking (budget_tokens). @@ -88,13 +88,13 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte if ok { switch budget { case 0: - out, _ = sjson.Set(out, "thinking.type", "disabled") + out, _ = sjson.SetBytes(out, "thinking.type", "disabled") case -1: - out, _ = sjson.Set(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") default: if budget > 0 { - out, _ = sjson.Set(out, "thinking.type", "enabled") - out, _ = sjson.Set(out, "thinking.budget_tokens", budget) + out, _ = sjson.SetBytes(out, "thinking.type", "enabled") + out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget) } } } @@ -114,15 +114,15 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte } // Model - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Max tokens if mot := root.Get("max_output_tokens"); mot.Exists() { - out, _ = sjson.Set(out, "max_tokens", mot.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", mot.Int()) } // Stream - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // instructions -> as a leading message (use role user for Claude API compatibility) instructionsText := "" @@ -130,9 +130,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String { instructionsText = instr.String() if instructionsText != "" { - sysMsg := `{"role":"user","content":""}` - sysMsg, _ = sjson.Set(sysMsg, "content", instructionsText) - out, _ = sjson.SetRaw(out, "messages.-1", sysMsg) + sysMsg := []byte(`{"role":"user","content":""}`) + sysMsg, _ = sjson.SetBytes(sysMsg, "content", instructionsText) + out, _ = sjson.SetRawBytes(out, "messages.-1", sysMsg) } } @@ -156,9 +156,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte } instructionsText = builder.String() if instructionsText != "" { - sysMsg := `{"role":"user","content":""}` - sysMsg, _ = sjson.Set(sysMsg, "content", instructionsText) - out, _ = sjson.SetRaw(out, "messages.-1", sysMsg) + sysMsg := []byte(`{"role":"user","content":""}`) + sysMsg, _ = sjson.SetBytes(sysMsg, "content", instructionsText) + out, _ = sjson.SetRawBytes(out, "messages.-1", sysMsg) extractedFromSystem = true } } @@ -193,9 +193,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte if t := part.Get("text"); t.Exists() { txt := t.String() textAggregate.WriteString(txt) - contentPart := `{"type":"text","text":""}` - contentPart, _ = sjson.Set(contentPart, "text", txt) - partsJSON = append(partsJSON, contentPart) + contentPart := []byte(`{"type":"text","text":""}`) + contentPart, _ = sjson.SetBytes(contentPart, "text", txt) + partsJSON = append(partsJSON, string(contentPart)) } if ptype == "input_text" { role = "user" @@ -208,7 +208,7 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte url = part.Get("url").String() } if url != "" { - var contentPart string + var contentPart []byte if strings.HasPrefix(url, "data:") { trimmed := strings.TrimPrefix(url, "data:") mediaAndData := strings.SplitN(trimmed, ";base64,", 2) @@ -221,16 +221,16 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte data = mediaAndData[1] } if data != "" { - contentPart = `{"type":"image","source":{"type":"base64","media_type":"","data":""}}` - contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType) - contentPart, _ = sjson.Set(contentPart, "source.data", data) + contentPart = []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`) + contentPart, _ = sjson.SetBytes(contentPart, "source.media_type", mediaType) + contentPart, _ = sjson.SetBytes(contentPart, "source.data", data) } } else { - contentPart = `{"type":"image","source":{"type":"url","url":""}}` - contentPart, _ = sjson.Set(contentPart, "source.url", url) + contentPart = []byte(`{"type":"image","source":{"type":"url","url":""}}`) + contentPart, _ = sjson.SetBytes(contentPart, "source.url", url) } - if contentPart != "" { - partsJSON = append(partsJSON, contentPart) + if len(contentPart) > 0 { + partsJSON = append(partsJSON, string(contentPart)) if role == "" { role = "user" } @@ -252,10 +252,10 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte data = mediaAndData[1] } } - contentPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}` - contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType) - contentPart, _ = sjson.Set(contentPart, "source.data", data) - partsJSON = append(partsJSON, contentPart) + contentPart := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`) + contentPart, _ = sjson.SetBytes(contentPart, "source.media_type", mediaType) + contentPart, _ = sjson.SetBytes(contentPart, "source.data", data) + partsJSON = append(partsJSON, string(contentPart)) if role == "" { role = "user" } @@ -280,24 +280,24 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte } if len(partsJSON) > 0 { - msg := `{"role":"","content":[]}` - msg, _ = sjson.Set(msg, "role", role) + msg := []byte(`{"role":"","content":[]}`) + msg, _ = sjson.SetBytes(msg, "role", role) if len(partsJSON) == 1 && !hasImage && !hasFile { // Preserve legacy behavior for single text content - msg, _ = sjson.Delete(msg, "content") + msg, _ = sjson.DeleteBytes(msg, "content") textPart := gjson.Parse(partsJSON[0]) - msg, _ = sjson.Set(msg, "content", textPart.Get("text").String()) + msg, _ = sjson.SetBytes(msg, "content", textPart.Get("text").String()) } else { for _, partJSON := range partsJSON { - msg, _ = sjson.SetRaw(msg, "content.-1", partJSON) + msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(partJSON)) } } - out, _ = sjson.SetRaw(out, "messages.-1", msg) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } else if textAggregate.Len() > 0 || role == "system" { - msg := `{"role":"","content":""}` - msg, _ = sjson.Set(msg, "role", role) - msg, _ = sjson.Set(msg, "content", textAggregate.String()) - out, _ = sjson.SetRaw(out, "messages.-1", msg) + msg := []byte(`{"role":"","content":""}`) + msg, _ = sjson.SetBytes(msg, "role", role) + msg, _ = sjson.SetBytes(msg, "content", textAggregate.String()) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } case "function_call": @@ -309,31 +309,31 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte name := item.Get("name").String() argsStr := item.Get("arguments").String() - toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` - toolUse, _ = sjson.Set(toolUse, "id", callID) - toolUse, _ = sjson.Set(toolUse, "name", name) + toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolUse, _ = sjson.SetBytes(toolUse, "id", callID) + toolUse, _ = sjson.SetBytes(toolUse, "name", name) if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { - toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw) + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw)) } } - asst := `{"role":"assistant","content":[]}` - asst, _ = sjson.SetRaw(asst, "content.-1", toolUse) - out, _ = sjson.SetRaw(out, "messages.-1", asst) + asst := []byte(`{"role":"assistant","content":[]}`) + asst, _ = sjson.SetRawBytes(asst, "content.-1", toolUse) + out, _ = sjson.SetRawBytes(out, "messages.-1", asst) case "function_call_output": // Map to user tool_result callID := item.Get("call_id").String() outputStr := item.Get("output").String() - toolResult := `{"type":"tool_result","tool_use_id":"","content":""}` - toolResult, _ = sjson.Set(toolResult, "tool_use_id", callID) - toolResult, _ = sjson.Set(toolResult, "content", outputStr) + toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`) + toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", callID) + toolResult, _ = sjson.SetBytes(toolResult, "content", outputStr) - usr := `{"role":"user","content":[]}` - usr, _ = sjson.SetRaw(usr, "content.-1", toolResult) - out, _ = sjson.SetRaw(out, "messages.-1", usr) + usr := []byte(`{"role":"user","content":[]}`) + usr, _ = sjson.SetRawBytes(usr, "content.-1", toolResult) + out, _ = sjson.SetRawBytes(out, "messages.-1", usr) } return true }) @@ -341,27 +341,27 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte // tools mapping: parameters -> input_schema if tools := root.Get("tools"); tools.Exists() && tools.IsArray() { - toolsJSON := "[]" + toolsJSON := []byte("[]") tools.ForEach(func(_, tool gjson.Result) bool { - tJSON := `{"name":"","description":"","input_schema":{}}` + tJSON := []byte(`{"name":"","description":"","input_schema":{}}`) if n := tool.Get("name"); n.Exists() { - tJSON, _ = sjson.Set(tJSON, "name", n.String()) + tJSON, _ = sjson.SetBytes(tJSON, "name", n.String()) } if d := tool.Get("description"); d.Exists() { - tJSON, _ = sjson.Set(tJSON, "description", d.String()) + tJSON, _ = sjson.SetBytes(tJSON, "description", d.String()) } if params := tool.Get("parameters"); params.Exists() { - tJSON, _ = sjson.SetRaw(tJSON, "input_schema", params.Raw) + tJSON, _ = sjson.SetRawBytes(tJSON, "input_schema", []byte(params.Raw)) } else if params = tool.Get("parametersJsonSchema"); params.Exists() { - tJSON, _ = sjson.SetRaw(tJSON, "input_schema", params.Raw) + tJSON, _ = sjson.SetRawBytes(tJSON, "input_schema", []byte(params.Raw)) } - toolsJSON, _ = sjson.SetRaw(toolsJSON, "-1", tJSON) + toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", tJSON) return true }) - if gjson.Parse(toolsJSON).IsArray() && len(gjson.Parse(toolsJSON).Array()) > 0 { - out, _ = sjson.SetRaw(out, "tools", toolsJSON) + if parsedTools := gjson.ParseBytes(toolsJSON); parsedTools.IsArray() && len(parsedTools.Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "tools", toolsJSON) } } @@ -371,23 +371,23 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte case gjson.String: switch toolChoice.String() { case "auto": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`)) case "none": // Leave unset; implies no tools case "required": - out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`)) } case gjson.JSON: if toolChoice.Get("type").String() == "function" { fn := toolChoice.Get("function.name").String() - toolChoiceJSON := `{"name":"","type":"tool"}` - toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", fn) - out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON) + toolChoiceJSON := []byte(`{"name":"","type":"tool"}`) + toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "name", fn) + out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON) } default: } } - return []byte(out) + return out } diff --git a/internal/translator/claude/openai/responses/claude_openai-responses_response.go b/internal/translator/claude/openai/responses/claude_openai-responses_response.go index e77b09e1..ef2cc1f8 100644 --- a/internal/translator/claude/openai/responses/claude_openai-responses_response.go +++ b/internal/translator/claude/openai/responses/claude_openai-responses_response.go @@ -8,6 +8,7 @@ import ( "strings" "time" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -50,12 +51,12 @@ func pickRequestJSON(originalRequestRawJSON, requestRawJSON []byte) []byte { return nil } -func emitEvent(event string, payload string) string { - return fmt.Sprintf("event: %s\ndata: %s", event, payload) +func emitEvent(event string, payload []byte) []byte { + return translatorcommon.SSEEventData(event, payload) } // ConvertClaudeResponseToOpenAIResponses converts Claude SSE to OpenAI Responses SSE events. -func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &claudeToResponsesState{FuncArgsBuf: make(map[int]*strings.Builder), FuncNames: make(map[int]string), FuncCallIDs: make(map[int]string)} } @@ -63,12 +64,12 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin // Expect `data: {..}` from Claude clients if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) root := gjson.ParseBytes(rawJSON) ev := root.Get("type").String() - var out []string + var out [][]byte nextSeq := func() int { st.Seq++; return st.Seq } @@ -105,16 +106,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin } } // response.created - created := `{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}` - created, _ = sjson.Set(created, "sequence_number", nextSeq()) - created, _ = sjson.Set(created, "response.id", st.ResponseID) - created, _ = sjson.Set(created, "response.created_at", st.CreatedAt) + created := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`) + created, _ = sjson.SetBytes(created, "sequence_number", nextSeq()) + created, _ = sjson.SetBytes(created, "response.id", st.ResponseID) + created, _ = sjson.SetBytes(created, "response.created_at", st.CreatedAt) out = append(out, emitEvent("response.created", created)) // response.in_progress - inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}` - inprog, _ = sjson.Set(inprog, "sequence_number", nextSeq()) - inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID) - inprog, _ = sjson.Set(inprog, "response.created_at", st.CreatedAt) + inprog := []byte(`{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`) + inprog, _ = sjson.SetBytes(inprog, "sequence_number", nextSeq()) + inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID) + inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.CreatedAt) out = append(out, emitEvent("response.in_progress", inprog)) } case "content_block_start": @@ -128,25 +129,25 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin // open message item + content part st.InTextBlock = true st.CurrentMsgID = fmt.Sprintf("msg_%s_0", st.ResponseID) - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "item.id", st.CurrentMsgID) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "item.id", st.CurrentMsgID) out = append(out, emitEvent("response.output_item.added", item)) - part := `{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - part, _ = sjson.Set(part, "sequence_number", nextSeq()) - part, _ = sjson.Set(part, "item_id", st.CurrentMsgID) + part := []byte(`{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + part, _ = sjson.SetBytes(part, "sequence_number", nextSeq()) + part, _ = sjson.SetBytes(part, "item_id", st.CurrentMsgID) out = append(out, emitEvent("response.content_part.added", part)) } else if typ == "tool_use" { st.InFuncBlock = true st.CurrentFCID = cb.Get("id").String() name := cb.Get("name").String() - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", idx) - item, _ = sjson.Set(item, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID)) - item, _ = sjson.Set(item, "item.call_id", st.CurrentFCID) - item, _ = sjson.Set(item, "item.name", name) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID)) + item, _ = sjson.SetBytes(item, "item.call_id", st.CurrentFCID) + item, _ = sjson.SetBytes(item, "item.name", name) out = append(out, emitEvent("response.output_item.added", item)) if st.FuncArgsBuf[idx] == nil { st.FuncArgsBuf[idx] = &strings.Builder{} @@ -160,16 +161,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin st.ReasoningIndex = idx st.ReasoningBuf.Reset() st.ReasoningItemID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx) - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", idx) - item, _ = sjson.Set(item, "item.id", st.ReasoningItemID) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "item.id", st.ReasoningItemID) out = append(out, emitEvent("response.output_item.added", item)) // add a summary part placeholder - part := `{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}` - part, _ = sjson.Set(part, "sequence_number", nextSeq()) - part, _ = sjson.Set(part, "item_id", st.ReasoningItemID) - part, _ = sjson.Set(part, "output_index", idx) + part := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) + part, _ = sjson.SetBytes(part, "sequence_number", nextSeq()) + part, _ = sjson.SetBytes(part, "item_id", st.ReasoningItemID) + part, _ = sjson.SetBytes(part, "output_index", idx) out = append(out, emitEvent("response.reasoning_summary_part.added", part)) st.ReasoningPartAdded = true } @@ -181,10 +182,10 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin dt := d.Get("type").String() if dt == "text_delta" { if t := d.Get("text"); t.Exists() { - msg := `{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", st.CurrentMsgID) - msg, _ = sjson.Set(msg, "delta", t.String()) + msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", st.CurrentMsgID) + msg, _ = sjson.SetBytes(msg, "delta", t.String()) out = append(out, emitEvent("response.output_text.delta", msg)) // aggregate text for response.output st.TextBuf.WriteString(t.String()) @@ -196,22 +197,22 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin st.FuncArgsBuf[idx] = &strings.Builder{} } st.FuncArgsBuf[idx].WriteString(pj.String()) - msg := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID)) - msg, _ = sjson.Set(msg, "output_index", idx) - msg, _ = sjson.Set(msg, "delta", pj.String()) + msg := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID)) + msg, _ = sjson.SetBytes(msg, "output_index", idx) + msg, _ = sjson.SetBytes(msg, "delta", pj.String()) out = append(out, emitEvent("response.function_call_arguments.delta", msg)) } } else if dt == "thinking_delta" { if st.ReasoningActive { if t := d.Get("thinking"); t.Exists() { st.ReasoningBuf.WriteString(t.String()) - msg := `{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", st.ReasoningItemID) - msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex) - msg, _ = sjson.Set(msg, "delta", t.String()) + msg := []byte(`{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningItemID) + msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex) + msg, _ = sjson.SetBytes(msg, "delta", t.String()) out = append(out, emitEvent("response.reasoning_summary_text.delta", msg)) } } @@ -219,17 +220,17 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin case "content_block_stop": idx := int(root.Get("index").Int()) if st.InTextBlock { - done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}` - done, _ = sjson.Set(done, "sequence_number", nextSeq()) - done, _ = sjson.Set(done, "item_id", st.CurrentMsgID) + done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) + done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) + done, _ = sjson.SetBytes(done, "item_id", st.CurrentMsgID) out = append(out, emitEvent("response.output_text.done", done)) - partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", st.CurrentMsgID) + partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", st.CurrentMsgID) out = append(out, emitEvent("response.content_part.done", partDone)) - final := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","text":""}],"role":"assistant"}}` - final, _ = sjson.Set(final, "sequence_number", nextSeq()) - final, _ = sjson.Set(final, "item.id", st.CurrentMsgID) + final := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","text":""}],"role":"assistant"}}`) + final, _ = sjson.SetBytes(final, "sequence_number", nextSeq()) + final, _ = sjson.SetBytes(final, "item.id", st.CurrentMsgID) out = append(out, emitEvent("response.output_item.done", final)) st.InTextBlock = false } else if st.InFuncBlock { @@ -239,34 +240,34 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin args = buf.String() } } - fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}` - fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq()) - fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID)) - fcDone, _ = sjson.Set(fcDone, "output_index", idx) - fcDone, _ = sjson.Set(fcDone, "arguments", args) + fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`) + fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq()) + fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID)) + fcDone, _ = sjson.SetBytes(fcDone, "output_index", idx) + fcDone, _ = sjson.SetBytes(fcDone, "arguments", args) out = append(out, emitEvent("response.function_call_arguments.done", fcDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "output_index", idx) - itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID)) - itemDone, _ = sjson.Set(itemDone, "item.arguments", args) - itemDone, _ = sjson.Set(itemDone, "item.call_id", st.CurrentFCID) - itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[idx]) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID)) + itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args) + itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", st.CurrentFCID) + itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[idx]) out = append(out, emitEvent("response.output_item.done", itemDone)) st.InFuncBlock = false } else if st.ReasoningActive { full := st.ReasoningBuf.String() - textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}` - textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq()) - textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningItemID) - textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex) - textDone, _ = sjson.Set(textDone, "text", full) + textDone := []byte(`{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`) + textDone, _ = sjson.SetBytes(textDone, "sequence_number", nextSeq()) + textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningItemID) + textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex) + textDone, _ = sjson.SetBytes(textDone, "text", full) out = append(out, emitEvent("response.reasoning_summary_text.done", textDone)) - partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningItemID) - partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex) - partDone, _ = sjson.Set(partDone, "part.text", full) + partDone := []byte(`{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningItemID) + partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex) + partDone, _ = sjson.SetBytes(partDone, "part.text", full) out = append(out, emitEvent("response.reasoning_summary_part.done", partDone)) st.ReasoningActive = false st.ReasoningPartAdded = false @@ -284,92 +285,92 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin } case "message_stop": - completed := `{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}` - completed, _ = sjson.Set(completed, "sequence_number", nextSeq()) - completed, _ = sjson.Set(completed, "response.id", st.ResponseID) - completed, _ = sjson.Set(completed, "response.created_at", st.CreatedAt) + completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`) + completed, _ = sjson.SetBytes(completed, "sequence_number", nextSeq()) + completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID) + completed, _ = sjson.SetBytes(completed, "response.created_at", st.CreatedAt) // Inject original request fields into response as per docs/response.completed.json reqBytes := pickRequestJSON(originalRequestRawJSON, requestRawJSON) if len(reqBytes) > 0 { req := gjson.ParseBytes(reqBytes) if v := req.Get("instructions"); v.Exists() { - completed, _ = sjson.Set(completed, "response.instructions", v.String()) + completed, _ = sjson.SetBytes(completed, "response.instructions", v.String()) } if v := req.Get("max_output_tokens"); v.Exists() { - completed, _ = sjson.Set(completed, "response.max_output_tokens", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.max_output_tokens", v.Int()) } if v := req.Get("max_tool_calls"); v.Exists() { - completed, _ = sjson.Set(completed, "response.max_tool_calls", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.max_tool_calls", v.Int()) } if v := req.Get("model"); v.Exists() { - completed, _ = sjson.Set(completed, "response.model", v.String()) + completed, _ = sjson.SetBytes(completed, "response.model", v.String()) } if v := req.Get("parallel_tool_calls"); v.Exists() { - completed, _ = sjson.Set(completed, "response.parallel_tool_calls", v.Bool()) + completed, _ = sjson.SetBytes(completed, "response.parallel_tool_calls", v.Bool()) } if v := req.Get("previous_response_id"); v.Exists() { - completed, _ = sjson.Set(completed, "response.previous_response_id", v.String()) + completed, _ = sjson.SetBytes(completed, "response.previous_response_id", v.String()) } if v := req.Get("prompt_cache_key"); v.Exists() { - completed, _ = sjson.Set(completed, "response.prompt_cache_key", v.String()) + completed, _ = sjson.SetBytes(completed, "response.prompt_cache_key", v.String()) } if v := req.Get("reasoning"); v.Exists() { - completed, _ = sjson.Set(completed, "response.reasoning", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.reasoning", v.Value()) } if v := req.Get("safety_identifier"); v.Exists() { - completed, _ = sjson.Set(completed, "response.safety_identifier", v.String()) + completed, _ = sjson.SetBytes(completed, "response.safety_identifier", v.String()) } if v := req.Get("service_tier"); v.Exists() { - completed, _ = sjson.Set(completed, "response.service_tier", v.String()) + completed, _ = sjson.SetBytes(completed, "response.service_tier", v.String()) } if v := req.Get("store"); v.Exists() { - completed, _ = sjson.Set(completed, "response.store", v.Bool()) + completed, _ = sjson.SetBytes(completed, "response.store", v.Bool()) } if v := req.Get("temperature"); v.Exists() { - completed, _ = sjson.Set(completed, "response.temperature", v.Float()) + completed, _ = sjson.SetBytes(completed, "response.temperature", v.Float()) } if v := req.Get("text"); v.Exists() { - completed, _ = sjson.Set(completed, "response.text", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.text", v.Value()) } if v := req.Get("tool_choice"); v.Exists() { - completed, _ = sjson.Set(completed, "response.tool_choice", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.tool_choice", v.Value()) } if v := req.Get("tools"); v.Exists() { - completed, _ = sjson.Set(completed, "response.tools", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.tools", v.Value()) } if v := req.Get("top_logprobs"); v.Exists() { - completed, _ = sjson.Set(completed, "response.top_logprobs", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.top_logprobs", v.Int()) } if v := req.Get("top_p"); v.Exists() { - completed, _ = sjson.Set(completed, "response.top_p", v.Float()) + completed, _ = sjson.SetBytes(completed, "response.top_p", v.Float()) } if v := req.Get("truncation"); v.Exists() { - completed, _ = sjson.Set(completed, "response.truncation", v.String()) + completed, _ = sjson.SetBytes(completed, "response.truncation", v.String()) } if v := req.Get("user"); v.Exists() { - completed, _ = sjson.Set(completed, "response.user", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.user", v.Value()) } if v := req.Get("metadata"); v.Exists() { - completed, _ = sjson.Set(completed, "response.metadata", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.metadata", v.Value()) } } // Build response.output from aggregated state - outputsWrapper := `{"arr":[]}` + outputsWrapper := []byte(`{"arr":[]}`) // reasoning item (if any) if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded { - item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` - item, _ = sjson.Set(item, "id", st.ReasoningItemID) - item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`) + item, _ = sjson.SetBytes(item, "id", st.ReasoningItemID) + item, _ = sjson.SetBytes(item, "summary.0.text", st.ReasoningBuf.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } // assistant message item (if any text) if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" { - item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` - item, _ = sjson.Set(item, "id", st.CurrentMsgID) - item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) + item, _ = sjson.SetBytes(item, "id", st.CurrentMsgID) + item, _ = sjson.SetBytes(item, "content.0.text", st.TextBuf.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } // function_call items (in ascending index order for determinism) if len(st.FuncArgsBuf) > 0 { @@ -396,16 +397,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin if callID == "" && st.CurrentFCID != "" { callID = st.CurrentFCID } - item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) - item, _ = sjson.Set(item, "arguments", args) - item, _ = sjson.Set(item, "call_id", callID) - item, _ = sjson.Set(item, "name", name) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID)) + item, _ = sjson.SetBytes(item, "arguments", args) + item, _ = sjson.SetBytes(item, "call_id", callID) + item, _ = sjson.SetBytes(item, "name", name) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } } - if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { - completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw) + if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { + completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) } reasoningTokens := int64(0) @@ -414,15 +415,15 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin } usagePresent := st.UsageSeen || reasoningTokens > 0 if usagePresent { - completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.InputTokens) - completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", 0) - completed, _ = sjson.Set(completed, "response.usage.output_tokens", st.OutputTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", st.InputTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens_details.cached_tokens", 0) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens", st.OutputTokens) if reasoningTokens > 0 { - completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", reasoningTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens_details.reasoning_tokens", reasoningTokens) } total := st.InputTokens + st.OutputTokens if total > 0 || st.UsageSeen { - completed, _ = sjson.Set(completed, "response.usage.total_tokens", total) + completed, _ = sjson.SetBytes(completed, "response.usage.total_tokens", total) } } out = append(out, emitEvent("response.completed", completed)) @@ -432,7 +433,7 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin } // ConvertClaudeResponseToOpenAIResponsesNonStream aggregates Claude SSE into a single OpenAI Responses JSON. -func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { // Aggregate Claude SSE lines into a single OpenAI Responses JSON (non-stream) // We follow the same aggregation logic as the streaming variant but produce // one final object matching docs/out.json structure. @@ -455,7 +456,7 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string } // Base OpenAI Responses (non-stream) object - out := `{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null,"output":[],"usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{},"total_tokens":0}}` + out := []byte(`{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null,"output":[],"usage":{"input_tokens":0,"input_tokens_details":{"cached_tokens":0},"output_tokens":0,"output_tokens_details":{},"total_tokens":0}}`) // Aggregation state var ( @@ -557,88 +558,88 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string } // Populate base fields - out, _ = sjson.Set(out, "id", responseID) - out, _ = sjson.Set(out, "created_at", createdAt) + out, _ = sjson.SetBytes(out, "id", responseID) + out, _ = sjson.SetBytes(out, "created_at", createdAt) // Inject request echo fields as top-level (similar to streaming variant) reqBytes := pickRequestJSON(originalRequestRawJSON, requestRawJSON) if len(reqBytes) > 0 { req := gjson.ParseBytes(reqBytes) if v := req.Get("instructions"); v.Exists() { - out, _ = sjson.Set(out, "instructions", v.String()) + out, _ = sjson.SetBytes(out, "instructions", v.String()) } if v := req.Get("max_output_tokens"); v.Exists() { - out, _ = sjson.Set(out, "max_output_tokens", v.Int()) + out, _ = sjson.SetBytes(out, "max_output_tokens", v.Int()) } if v := req.Get("max_tool_calls"); v.Exists() { - out, _ = sjson.Set(out, "max_tool_calls", v.Int()) + out, _ = sjson.SetBytes(out, "max_tool_calls", v.Int()) } if v := req.Get("model"); v.Exists() { - out, _ = sjson.Set(out, "model", v.String()) + out, _ = sjson.SetBytes(out, "model", v.String()) } if v := req.Get("parallel_tool_calls"); v.Exists() { - out, _ = sjson.Set(out, "parallel_tool_calls", v.Bool()) + out, _ = sjson.SetBytes(out, "parallel_tool_calls", v.Bool()) } if v := req.Get("previous_response_id"); v.Exists() { - out, _ = sjson.Set(out, "previous_response_id", v.String()) + out, _ = sjson.SetBytes(out, "previous_response_id", v.String()) } if v := req.Get("prompt_cache_key"); v.Exists() { - out, _ = sjson.Set(out, "prompt_cache_key", v.String()) + out, _ = sjson.SetBytes(out, "prompt_cache_key", v.String()) } if v := req.Get("reasoning"); v.Exists() { - out, _ = sjson.Set(out, "reasoning", v.Value()) + out, _ = sjson.SetBytes(out, "reasoning", v.Value()) } if v := req.Get("safety_identifier"); v.Exists() { - out, _ = sjson.Set(out, "safety_identifier", v.String()) + out, _ = sjson.SetBytes(out, "safety_identifier", v.String()) } if v := req.Get("service_tier"); v.Exists() { - out, _ = sjson.Set(out, "service_tier", v.String()) + out, _ = sjson.SetBytes(out, "service_tier", v.String()) } if v := req.Get("store"); v.Exists() { - out, _ = sjson.Set(out, "store", v.Bool()) + out, _ = sjson.SetBytes(out, "store", v.Bool()) } if v := req.Get("temperature"); v.Exists() { - out, _ = sjson.Set(out, "temperature", v.Float()) + out, _ = sjson.SetBytes(out, "temperature", v.Float()) } if v := req.Get("text"); v.Exists() { - out, _ = sjson.Set(out, "text", v.Value()) + out, _ = sjson.SetBytes(out, "text", v.Value()) } if v := req.Get("tool_choice"); v.Exists() { - out, _ = sjson.Set(out, "tool_choice", v.Value()) + out, _ = sjson.SetBytes(out, "tool_choice", v.Value()) } if v := req.Get("tools"); v.Exists() { - out, _ = sjson.Set(out, "tools", v.Value()) + out, _ = sjson.SetBytes(out, "tools", v.Value()) } if v := req.Get("top_logprobs"); v.Exists() { - out, _ = sjson.Set(out, "top_logprobs", v.Int()) + out, _ = sjson.SetBytes(out, "top_logprobs", v.Int()) } if v := req.Get("top_p"); v.Exists() { - out, _ = sjson.Set(out, "top_p", v.Float()) + out, _ = sjson.SetBytes(out, "top_p", v.Float()) } if v := req.Get("truncation"); v.Exists() { - out, _ = sjson.Set(out, "truncation", v.String()) + out, _ = sjson.SetBytes(out, "truncation", v.String()) } if v := req.Get("user"); v.Exists() { - out, _ = sjson.Set(out, "user", v.Value()) + out, _ = sjson.SetBytes(out, "user", v.Value()) } if v := req.Get("metadata"); v.Exists() { - out, _ = sjson.Set(out, "metadata", v.Value()) + out, _ = sjson.SetBytes(out, "metadata", v.Value()) } } // Build output array - outputsWrapper := `{"arr":[]}` + outputsWrapper := []byte(`{"arr":[]}`) if reasoningBuf.Len() > 0 { - item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` - item, _ = sjson.Set(item, "id", reasoningItemID) - item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`) + item, _ = sjson.SetBytes(item, "id", reasoningItemID) + item, _ = sjson.SetBytes(item, "summary.0.text", reasoningBuf.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } if currentMsgID != "" || textBuf.Len() > 0 { - item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` - item, _ = sjson.Set(item, "id", currentMsgID) - item, _ = sjson.Set(item, "content.0.text", textBuf.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) + item, _ = sjson.SetBytes(item, "id", currentMsgID) + item, _ = sjson.SetBytes(item, "content.0.text", textBuf.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } if len(toolCalls) > 0 { // Preserve index order @@ -659,28 +660,28 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string if args == "" { args = "{}" } - item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id)) - item, _ = sjson.Set(item, "arguments", args) - item, _ = sjson.Set(item, "call_id", st.id) - item, _ = sjson.Set(item, "name", st.name) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", st.id)) + item, _ = sjson.SetBytes(item, "arguments", args) + item, _ = sjson.SetBytes(item, "call_id", st.id) + item, _ = sjson.SetBytes(item, "name", st.name) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } } - if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { - out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw) + if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { + out, _ = sjson.SetRawBytes(out, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) } // Usage total := inputTokens + outputTokens - out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) - out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) - out, _ = sjson.Set(out, "usage.total_tokens", total) + out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.total_tokens", total) if reasoningBuf.Len() > 0 { // Rough estimate similar to chat completions reasoningTokens := int64(len(reasoningBuf.String()) / 4) if reasoningTokens > 0 { - out, _ = sjson.Set(out, "usage.output_tokens_details.reasoning_tokens", reasoningTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens_details.reasoning_tokens", reasoningTokens) } } diff --git a/internal/translator/codex/claude/codex_claude_request.go b/internal/translator/codex/claude/codex_claude_request.go index 4bc116b9..adff9a03 100644 --- a/internal/translator/codex/claude/codex_claude_request.go +++ b/internal/translator/codex/claude/codex_claude_request.go @@ -36,15 +36,15 @@ import ( func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := inputRawJSON - template := `{"model":"","instructions":"","input":[]}` + template := []byte(`{"model":"","instructions":"","input":[]}`) rootResult := gjson.ParseBytes(rawJSON) - template, _ = sjson.Set(template, "model", modelName) + template, _ = sjson.SetBytes(template, "model", modelName) // Process system messages and convert them to input content format. systemsResult := rootResult.Get("system") if systemsResult.Exists() { - message := `{"type":"message","role":"developer","content":[]}` + message := []byte(`{"type":"message","role":"developer","content":[]}`) contentIndex := 0 appendSystemText := func(text string) { @@ -52,8 +52,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) return } - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text") - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text) + message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text") + message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.text", contentIndex), text) contentIndex++ } @@ -70,7 +70,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } if contentIndex > 0 { - template, _ = sjson.SetRaw(template, "input.-1", message) + template, _ = sjson.SetRawBytes(template, "input.-1", message) } } @@ -83,9 +83,9 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) messageResult := messageResults[i] messageRole := messageResult.Get("role").String() - newMessage := func() string { - msg := `{"type": "message","role":"","content":[]}` - msg, _ = sjson.Set(msg, "role", messageRole) + newMessage := func() []byte { + msg := []byte(`{"type":"message","role":"","content":[]}`) + msg, _ = sjson.SetBytes(msg, "role", messageRole) return msg } @@ -95,7 +95,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) flushMessage := func() { if hasContent { - template, _ = sjson.SetRaw(template, "input.-1", message) + template, _ = sjson.SetRawBytes(template, "input.-1", message) message = newMessage() contentIndex = 0 hasContent = false @@ -107,15 +107,15 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) if messageRole == "assistant" { partType = "output_text" } - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), partType) - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.text", contentIndex), text) + message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), partType) + message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.text", contentIndex), text) contentIndex++ hasContent = true } appendImageContent := func(dataURL string) { - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image") - message, _ = sjson.Set(message, fmt.Sprintf("content.%d.image_url", contentIndex), dataURL) + message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image") + message, _ = sjson.SetBytes(message, fmt.Sprintf("content.%d.image_url", contentIndex), dataURL) contentIndex++ hasContent = true } @@ -151,8 +151,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } case "tool_use": flushMessage() - functionCallMessage := `{"type":"function_call"}` - functionCallMessage, _ = sjson.Set(functionCallMessage, "call_id", messageContentResult.Get("id").String()) + functionCallMessage := []byte(`{"type":"function_call"}`) + functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "call_id", messageContentResult.Get("id").String()) { name := messageContentResult.Get("name").String() toolMap := buildReverseMapFromClaudeOriginalToShort(rawJSON) @@ -161,19 +161,19 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } else { name = shortenNameIfNeeded(name) } - functionCallMessage, _ = sjson.Set(functionCallMessage, "name", name) + functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "name", name) } - functionCallMessage, _ = sjson.Set(functionCallMessage, "arguments", messageContentResult.Get("input").Raw) - template, _ = sjson.SetRaw(template, "input.-1", functionCallMessage) + functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "arguments", messageContentResult.Get("input").Raw) + template, _ = sjson.SetRawBytes(template, "input.-1", functionCallMessage) case "tool_result": flushMessage() - functionCallOutputMessage := `{"type":"function_call_output"}` - functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String()) + functionCallOutputMessage := []byte(`{"type":"function_call_output"}`) + functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String()) contentResult := messageContentResult.Get("content") if contentResult.IsArray() { toolResultContentIndex := 0 - toolResultContent := `[]` + toolResultContent := []byte(`[]`) contentResults := contentResult.Array() for k := 0; k < len(contentResults); k++ { toolResultContentType := contentResults[k].Get("type").String() @@ -194,27 +194,27 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data) - toolResultContent, _ = sjson.Set(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_image") - toolResultContent, _ = sjson.Set(toolResultContent, fmt.Sprintf("%d.image_url", toolResultContentIndex), dataURL) + toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_image") + toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.image_url", toolResultContentIndex), dataURL) toolResultContentIndex++ } } } else if toolResultContentType == "text" { - toolResultContent, _ = sjson.Set(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_text") - toolResultContent, _ = sjson.Set(toolResultContent, fmt.Sprintf("%d.text", toolResultContentIndex), contentResults[k].Get("text").String()) + toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_text") + toolResultContent, _ = sjson.SetBytes(toolResultContent, fmt.Sprintf("%d.text", toolResultContentIndex), contentResults[k].Get("text").String()) toolResultContentIndex++ } } - if toolResultContent != `[]` { - functionCallOutputMessage, _ = sjson.SetRaw(functionCallOutputMessage, "output", toolResultContent) + if toolResultContentIndex > 0 { + functionCallOutputMessage, _ = sjson.SetRawBytes(functionCallOutputMessage, "output", toolResultContent) } else { - functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "output", messageContentResult.Get("content").String()) + functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String()) } } else { - functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "output", messageContentResult.Get("content").String()) + functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String()) } - template, _ = sjson.SetRaw(template, "input.-1", functionCallOutputMessage) + template, _ = sjson.SetRawBytes(template, "input.-1", functionCallOutputMessage) } } flushMessage() @@ -229,8 +229,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Convert tools declarations to the expected format for the Codex API. toolsResult := rootResult.Get("tools") if toolsResult.IsArray() { - template, _ = sjson.SetRaw(template, "tools", `[]`) - template, _ = sjson.Set(template, "tool_choice", `auto`) + template, _ = sjson.SetRawBytes(template, "tools", []byte(`[]`)) + template, _ = sjson.SetBytes(template, "tool_choice", `auto`) toolResults := toolsResult.Array() // Build short name map from declared tools var names []string @@ -246,11 +246,11 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Special handling: map Claude web search tool to Codex web_search if toolResult.Get("type").String() == "web_search_20250305" { // Replace the tool content entirely with {"type":"web_search"} - template, _ = sjson.SetRaw(template, "tools.-1", `{"type":"web_search"}`) + template, _ = sjson.SetRawBytes(template, "tools.-1", []byte(`{"type":"web_search"}`)) continue } - tool := toolResult.Raw - tool, _ = sjson.Set(tool, "type", "function") + tool := []byte(toolResult.Raw) + tool, _ = sjson.SetBytes(tool, "type", "function") // Apply shortened name if needed if v := toolResult.Get("name"); v.Exists() { name := v.String() @@ -259,20 +259,26 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } else { name = shortenNameIfNeeded(name) } - tool, _ = sjson.Set(tool, "name", name) + tool, _ = sjson.SetBytes(tool, "name", name) } - tool, _ = sjson.SetRaw(tool, "parameters", normalizeToolParameters(toolResult.Get("input_schema").Raw)) - tool, _ = sjson.Delete(tool, "input_schema") - tool, _ = sjson.Delete(tool, "parameters.$schema") - tool, _ = sjson.Delete(tool, "cache_control") - tool, _ = sjson.Delete(tool, "defer_loading") - tool, _ = sjson.Set(tool, "strict", false) - template, _ = sjson.SetRaw(template, "tools.-1", tool) + tool, _ = sjson.SetRawBytes(tool, "parameters", []byte(normalizeToolParameters(toolResult.Get("input_schema").Raw))) + tool, _ = sjson.DeleteBytes(tool, "input_schema") + tool, _ = sjson.DeleteBytes(tool, "parameters.$schema") + tool, _ = sjson.DeleteBytes(tool, "cache_control") + tool, _ = sjson.DeleteBytes(tool, "defer_loading") + tool, _ = sjson.SetBytes(tool, "strict", false) + template, _ = sjson.SetRawBytes(template, "tools.-1", tool) } } + // Default to parallel tool calls unless tool_choice explicitly disables them. + parallelToolCalls := true + if disableParallelToolUse := rootResult.Get("tool_choice.disable_parallel_tool_use"); disableParallelToolUse.Exists() { + parallelToolCalls = !disableParallelToolUse.Bool() + } + // Add additional configuration parameters for the Codex API. - template, _ = sjson.Set(template, "parallel_tool_calls", true) + template, _ = sjson.SetBytes(template, "parallel_tool_calls", parallelToolCalls) // Convert thinking.budget_tokens to reasoning.effort. reasoningEffort := "medium" @@ -303,13 +309,13 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } } } - template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort) - template, _ = sjson.Set(template, "reasoning.summary", "auto") - template, _ = sjson.Set(template, "stream", true) - template, _ = sjson.Set(template, "store", false) - template, _ = sjson.Set(template, "include", []string{"reasoning.encrypted_content"}) + template, _ = sjson.SetBytes(template, "reasoning.effort", reasoningEffort) + template, _ = sjson.SetBytes(template, "reasoning.summary", "auto") + template, _ = sjson.SetBytes(template, "stream", true) + template, _ = sjson.SetBytes(template, "store", false) + template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"}) - return []byte(template) + return template } // shortenNameIfNeeded applies a simple shortening rule for a single name. @@ -412,15 +418,15 @@ func normalizeToolParameters(raw string) string { if raw == "" || raw == "null" || !gjson.Valid(raw) { return `{"type":"object","properties":{}}` } - schema := raw result := gjson.Parse(raw) + schema := []byte(raw) schemaType := result.Get("type").String() if schemaType == "" { - schema, _ = sjson.Set(schema, "type", "object") + schema, _ = sjson.SetBytes(schema, "type", "object") schemaType = "object" } if schemaType == "object" && !result.Get("properties").Exists() { - schema, _ = sjson.SetRaw(schema, "properties", `{}`) + schema, _ = sjson.SetRawBytes(schema, "properties", []byte(`{}`)) } - return schema + return string(schema) } diff --git a/internal/translator/codex/claude/codex_claude_request_test.go b/internal/translator/codex/claude/codex_claude_request_test.go index bdd41639..3cf02369 100644 --- a/internal/translator/codex/claude/codex_claude_request_test.go +++ b/internal/translator/codex/claude/codex_claude_request_test.go @@ -87,3 +87,49 @@ func TestConvertClaudeRequestToCodex_SystemMessageScenarios(t *testing.T) { }) } } + +func TestConvertClaudeRequestToCodex_ParallelToolCalls(t *testing.T) { + tests := []struct { + name string + inputJSON string + wantParallelToolCalls bool + }{ + { + name: "Default to true when tool_choice.disable_parallel_tool_use is absent", + inputJSON: `{ + "model": "claude-3-opus", + "messages": [{"role": "user", "content": "hello"}] + }`, + wantParallelToolCalls: true, + }, + { + name: "Disable parallel tool calls when client opts out", + inputJSON: `{ + "model": "claude-3-opus", + "tool_choice": {"disable_parallel_tool_use": true}, + "messages": [{"role": "user", "content": "hello"}] + }`, + wantParallelToolCalls: false, + }, + { + name: "Keep parallel tool calls enabled when client explicitly allows them", + inputJSON: `{ + "model": "claude-3-opus", + "tool_choice": {"disable_parallel_tool_use": false}, + "messages": [{"role": "user", "content": "hello"}] + }`, + wantParallelToolCalls: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ConvertClaudeRequestToCodex("test-model", []byte(tt.inputJSON), false) + resultJSON := gjson.ParseBytes(result) + + if got := resultJSON.Get("parallel_tool_calls").Bool(); got != tt.wantParallelToolCalls { + t.Fatalf("parallel_tool_calls = %v, want %v. Output: %s", got, tt.wantParallelToolCalls, string(result)) + } + }) + } +} diff --git a/internal/translator/codex/claude/codex_claude_response.go b/internal/translator/codex/claude/codex_claude_response.go index cf0fee46..b436cd3f 100644 --- a/internal/translator/codex/claude/codex_claude_response.go +++ b/internal/translator/codex/claude/codex_claude_response.go @@ -9,9 +9,9 @@ package claude import ( "bytes" "context" - "fmt" "strings" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -43,8 +43,8 @@ type ConvertCodexResponseToClaudeParams struct { // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Claude Code-compatible JSON response -func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Claude Code-compatible JSON responses +func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertCodexResponseToClaudeParams{ HasToolCall: false, @@ -54,95 +54,85 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa // log.Debugf("rawJSON: %s", string(rawJSON)) if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) - output := "" + output := make([]byte, 0, 512) rootResult := gjson.ParseBytes(rawJSON) typeResult := rootResult.Get("type") typeStr := typeResult.String() - template := "" + var template []byte if typeStr == "response.created" { - template = `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"claude-opus-4-1-20250805","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0},"content":[],"stop_reason":null}}` - template, _ = sjson.Set(template, "message.model", rootResult.Get("response.model").String()) - template, _ = sjson.Set(template, "message.id", rootResult.Get("response.id").String()) + template = []byte(`{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"claude-opus-4-1-20250805","stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0},"content":[],"stop_reason":null}}`) + template, _ = sjson.SetBytes(template, "message.model", rootResult.Get("response.model").String()) + template, _ = sjson.SetBytes(template, "message.id", rootResult.Get("response.id").String()) - output = "event: message_start\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "message_start", template, 2) } else if typeStr == "response.reasoning_summary_part.added" { - template = `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - output = "event: content_block_start\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2) } else if typeStr == "response.reasoning_summary_text.delta" { - template = `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - template, _ = sjson.Set(template, "delta.thinking", rootResult.Get("delta").String()) + template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.SetBytes(template, "delta.thinking", rootResult.Get("delta").String()) - output = "event: content_block_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2) } else if typeStr == "response.reasoning_summary_part.done" { - template = `{"type":"content_block_stop","index":0}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template = []byte(`{"type":"content_block_stop","index":0}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++ - output = "event: content_block_stop\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2) } else if typeStr == "response.content_part.added" { - template = `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - output = "event: content_block_start\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2) } else if typeStr == "response.output_text.delta" { - template = `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - template, _ = sjson.Set(template, "delta.text", rootResult.Get("delta").String()) + template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.SetBytes(template, "delta.text", rootResult.Get("delta").String()) - output = "event: content_block_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2) } else if typeStr == "response.content_part.done" { - template = `{"type":"content_block_stop","index":0}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template = []byte(`{"type":"content_block_stop","index":0}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++ - output = "event: content_block_stop\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2) } else if typeStr == "response.completed" { - template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template = []byte(`{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) p := (*param).(*ConvertCodexResponseToClaudeParams).HasToolCall stopReason := rootResult.Get("response.stop_reason").String() if p { - template, _ = sjson.Set(template, "delta.stop_reason", "tool_use") + template, _ = sjson.SetBytes(template, "delta.stop_reason", "tool_use") } else if stopReason == "max_tokens" || stopReason == "stop" { - template, _ = sjson.Set(template, "delta.stop_reason", stopReason) + template, _ = sjson.SetBytes(template, "delta.stop_reason", stopReason) } else { - template, _ = sjson.Set(template, "delta.stop_reason", "end_turn") + template, _ = sjson.SetBytes(template, "delta.stop_reason", "end_turn") } inputTokens, outputTokens, cachedTokens := extractResponsesUsage(rootResult.Get("response.usage")) - template, _ = sjson.Set(template, "usage.input_tokens", inputTokens) - template, _ = sjson.Set(template, "usage.output_tokens", outputTokens) + template, _ = sjson.SetBytes(template, "usage.input_tokens", inputTokens) + template, _ = sjson.SetBytes(template, "usage.output_tokens", outputTokens) if cachedTokens > 0 { - template, _ = sjson.Set(template, "usage.cache_read_input_tokens", cachedTokens) + template, _ = sjson.SetBytes(template, "usage.cache_read_input_tokens", cachedTokens) } - output = "event: message_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) - output += "event: message_stop\n" - output += `data: {"type":"message_stop"}` - output += "\n\n" + output = translatorcommon.AppendSSEEventBytes(output, "message_delta", template, 2) + output = translatorcommon.AppendSSEEventBytes(output, "message_stop", []byte(`{"type":"message_stop"}`), 2) } else if typeStr == "response.output_item.added" { itemResult := rootResult.Get("item") itemType := itemResult.Get("type").String() if itemType == "function_call" { (*param).(*ConvertCodexResponseToClaudeParams).HasToolCall = true (*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = false - template = `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - template, _ = sjson.Set(template, "content_block.id", util.SanitizeClaudeToolID(itemResult.Get("call_id").String())) + template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.SetBytes(template, "content_block.id", util.SanitizeClaudeToolID(itemResult.Get("call_id").String())) { // Restore original tool name if shortened name := itemResult.Get("name").String() @@ -150,37 +140,33 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa if orig, ok := rev[name]; ok { name = orig } - template, _ = sjson.Set(template, "content_block.name", name) + template, _ = sjson.SetBytes(template, "content_block.name", name) } - output = "event: content_block_start\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2) - template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - output += "event: content_block_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2) } } else if typeStr == "response.output_item.done" { itemResult := rootResult.Get("item") itemType := itemResult.Get("type").String() if itemType == "function_call" { - template = `{"type":"content_block_stop","index":0}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template = []byte(`{"type":"content_block_stop","index":0}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++ - output = "event: content_block_stop\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2) } } else if typeStr == "response.function_call_arguments.delta" { (*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = true - template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - template, _ = sjson.Set(template, "delta.partial_json", rootResult.Get("delta").String()) + template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.SetBytes(template, "delta.partial_json", rootResult.Get("delta").String()) - output += "event: content_block_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2) } else if typeStr == "response.function_call_arguments.done" { // Some models (e.g. gpt-5.3-codex-spark) send function call arguments // in a single "done" event without preceding "delta" events. @@ -189,17 +175,16 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa // When delta events were already received, skip to avoid duplicating arguments. if !(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta { if args := rootResult.Get("arguments").String(); args != "" { - template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) - template, _ = sjson.Set(template, "delta.partial_json", args) + template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`) + template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex) + template, _ = sjson.SetBytes(template, "delta.partial_json", args) - output += "event: content_block_delta\n" - output += fmt.Sprintf("data: %s\n\n", template) + output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2) } } } - return []string{output} + return [][]byte{output} } // ConvertCodexResponseToClaudeNonStream converts a non-streaming Codex response to a non-streaming Claude Code response. @@ -214,28 +199,28 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: A Claude Code-compatible JSON response containing all message content and metadata -func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) string { +// - []byte: A Claude Code-compatible JSON response containing all message content and metadata +func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) []byte { revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON) rootResult := gjson.ParseBytes(rawJSON) if rootResult.Get("type").String() != "response.completed" { - return "" + return []byte{} } responseData := rootResult.Get("response") if !responseData.Exists() { - return "" + return []byte{} } - out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` - out, _ = sjson.Set(out, "id", responseData.Get("id").String()) - out, _ = sjson.Set(out, "model", responseData.Get("model").String()) + out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) + out, _ = sjson.SetBytes(out, "id", responseData.Get("id").String()) + out, _ = sjson.SetBytes(out, "model", responseData.Get("model").String()) inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage")) - out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) - out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens) if cachedTokens > 0 { - out, _ = sjson.Set(out, "usage.cache_read_input_tokens", cachedTokens) + out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens) } hasToolCall := false @@ -276,9 +261,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original } } if thinkingBuilder.Len() > 0 { - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } case "message": if content := item.Get("content"); content.Exists() { @@ -287,9 +272,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original if part.Get("type").String() == "output_text" { text := part.Get("text").String() if text != "" { - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", text) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", text) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } } return true @@ -297,9 +282,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original } else { text := content.String() if text != "" { - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", text) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", text) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } } } @@ -310,9 +295,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original name = original } - toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` - toolBlock, _ = sjson.Set(toolBlock, "id", util.SanitizeClaudeToolID(item.Get("call_id").String())) - toolBlock, _ = sjson.Set(toolBlock, "name", name) + toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolBlock, _ = sjson.SetBytes(toolBlock, "id", util.SanitizeClaudeToolID(item.Get("call_id").String())) + toolBlock, _ = sjson.SetBytes(toolBlock, "name", name) inputRaw := "{}" if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) @@ -320,23 +305,23 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original inputRaw = argsJSON.Raw } } - toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) - out, _ = sjson.SetRaw(out, "content.-1", toolBlock) + toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw)) + out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock) } return true }) } if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" { - out, _ = sjson.Set(out, "stop_reason", stopReason.String()) + out, _ = sjson.SetBytes(out, "stop_reason", stopReason.String()) } else if hasToolCall { - out, _ = sjson.Set(out, "stop_reason", "tool_use") + out, _ = sjson.SetBytes(out, "stop_reason", "tool_use") } else { - out, _ = sjson.Set(out, "stop_reason", "end_turn") + out, _ = sjson.SetBytes(out, "stop_reason", "end_turn") } if stopSequence := responseData.Get("stop_sequence"); stopSequence.Exists() && stopSequence.String() != "" { - out, _ = sjson.SetRaw(out, "stop_sequence", stopSequence.Raw) + out, _ = sjson.SetRawBytes(out, "stop_sequence", []byte(stopSequence.Raw)) } return out @@ -386,6 +371,6 @@ func buildReverseMapFromClaudeOriginalShortToOriginal(original []byte) map[strin return rev } -func ClaudeTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"input_tokens":%d}`, count) +func ClaudeTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.ClaudeInputTokensJSON(count) } diff --git a/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go b/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go index c60e66b9..0f0068c8 100644 --- a/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go +++ b/internal/translator/codex/gemini-cli/codex_gemini-cli_response.go @@ -6,10 +6,9 @@ package geminiCLI import ( "context" - "fmt" . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini" - "github.com/tidwall/sjson" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" ) // ConvertCodexResponseToGeminiCLI converts Codex streaming response format to Gemini CLI format. @@ -24,14 +23,12 @@ import ( // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object -func ConvertCodexResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Gemini-compatible JSON responses wrapped in a response object +func ConvertCodexResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { outputs := ConvertCodexResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) - newOutputs := make([]string, 0) + newOutputs := make([][]byte, 0, len(outputs)) for i := 0; i < len(outputs); i++ { - json := `{"response": {}}` - output, _ := sjson.SetRaw(json, "response", outputs[i]) - newOutputs = append(newOutputs, output) + newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i])) } return newOutputs } @@ -47,15 +44,12 @@ func ConvertCodexResponseToGeminiCLI(ctx context.Context, modelName string, orig // - param: A pointer to a parameter object for the conversion // // Returns: -// - string: A Gemini-compatible JSON response wrapped in a response object -func ConvertCodexResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { - // log.Debug(string(rawJSON)) - strJSON := ConvertCodexResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) - json := `{"response": {}}` - strJSON, _ = sjson.SetRaw(json, "response", strJSON) - return strJSON +// - []byte: A Gemini-compatible JSON response wrapped in a response object +func ConvertCodexResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { + out := ConvertCodexResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) + return translatorcommon.WrapGeminiCLIResponse(out) } -func GeminiCLITokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiCLITokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } diff --git a/internal/translator/codex/gemini/codex_gemini_request.go b/internal/translator/codex/gemini/codex_gemini_request.go index 9f5d7b31..23dae7d7 100644 --- a/internal/translator/codex/gemini/codex_gemini_request.go +++ b/internal/translator/codex/gemini/codex_gemini_request.go @@ -38,7 +38,7 @@ import ( func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte { rawJSON := inputRawJSON // Base template - out := `{"model":"","instructions":"","input":[]}` + out := []byte(`{"model":"","instructions":"","input":[]}`) root := gjson.ParseBytes(rawJSON) @@ -82,24 +82,24 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } // Model - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // System instruction -> as a user message with input_text parts sysParts := root.Get("system_instruction.parts") if sysParts.IsArray() { - msg := `{"type":"message","role":"developer","content":[]}` + msg := []byte(`{"type":"message","role":"developer","content":[]}`) arr := sysParts.Array() for i := 0; i < len(arr); i++ { p := arr[i] if t := p.Get("text"); t.Exists() { - part := `{}` - part, _ = sjson.Set(part, "type", "input_text") - part, _ = sjson.Set(part, "text", t.String()) - msg, _ = sjson.SetRaw(msg, "content.-1", part) + part := []byte(`{}`) + part, _ = sjson.SetBytes(part, "type", "input_text") + part, _ = sjson.SetBytes(part, "text", t.String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) } } - if len(gjson.Get(msg, "content").Array()) > 0 { - out, _ = sjson.SetRaw(out, "input.-1", msg) + if len(gjson.GetBytes(msg, "content").Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "input.-1", msg) } } @@ -123,23 +123,23 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) p := parr[j] // text part if t := p.Get("text"); t.Exists() { - msg := `{"type":"message","role":"","content":[]}` - msg, _ = sjson.Set(msg, "role", role) + msg := []byte(`{"type":"message","role":"","content":[]}`) + msg, _ = sjson.SetBytes(msg, "role", role) partType := "input_text" if role == "assistant" { partType = "output_text" } - part := `{}` - part, _ = sjson.Set(part, "type", partType) - part, _ = sjson.Set(part, "text", t.String()) - msg, _ = sjson.SetRaw(msg, "content.-1", part) - out, _ = sjson.SetRaw(out, "input.-1", msg) + part := []byte(`{}`) + part, _ = sjson.SetBytes(part, "type", partType) + part, _ = sjson.SetBytes(part, "text", t.String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) + out, _ = sjson.SetRawBytes(out, "input.-1", msg) continue } // function call from model if fc := p.Get("functionCall"); fc.Exists() { - fn := `{"type":"function_call"}` + fn := []byte(`{"type":"function_call"}`) if name := fc.Get("name"); name.Exists() { n := name.String() if short, ok := shortMap[n]; ok { @@ -147,31 +147,31 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } else { n = shortenNameIfNeeded(n) } - fn, _ = sjson.Set(fn, "name", n) + fn, _ = sjson.SetBytes(fn, "name", n) } if args := fc.Get("args"); args.Exists() { - fn, _ = sjson.Set(fn, "arguments", args.Raw) + fn, _ = sjson.SetBytes(fn, "arguments", args.Raw) } // generate a paired random call_id and enqueue it so the // corresponding functionResponse can pop the earliest id // to preserve ordering when multiple calls are present. id := genCallID() - fn, _ = sjson.Set(fn, "call_id", id) + fn, _ = sjson.SetBytes(fn, "call_id", id) pendingCallIDs = append(pendingCallIDs, id) - out, _ = sjson.SetRaw(out, "input.-1", fn) + out, _ = sjson.SetRawBytes(out, "input.-1", fn) continue } // function response from user if fr := p.Get("functionResponse"); fr.Exists() { - fno := `{"type":"function_call_output"}` + fno := []byte(`{"type":"function_call_output"}`) // Prefer a string result if present; otherwise embed the raw response as a string if res := fr.Get("response.result"); res.Exists() { - fno, _ = sjson.Set(fno, "output", res.String()) + fno, _ = sjson.SetBytes(fno, "output", res.String()) } else if resp := fr.Get("response"); resp.Exists() { - fno, _ = sjson.Set(fno, "output", resp.Raw) + fno, _ = sjson.SetBytes(fno, "output", resp.Raw) } - // fno, _ = sjson.Set(fno, "call_id", "call_W6nRJzFXyPM2LFBbfo98qAbq") + // fno, _ = sjson.SetBytes(fno, "call_id", "call_W6nRJzFXyPM2LFBbfo98qAbq") // attach the oldest queued call_id to pair the response // with its call. If the queue is empty, generate a new id. var id string @@ -182,8 +182,8 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } else { id = genCallID() } - fno, _ = sjson.Set(fno, "call_id", id) - out, _ = sjson.SetRaw(out, "input.-1", fno) + fno, _ = sjson.SetBytes(fno, "call_id", id) + out, _ = sjson.SetRawBytes(out, "input.-1", fno) continue } } @@ -193,8 +193,8 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) // Tools mapping: Gemini functionDeclarations -> Codex tools tools := root.Get("tools") if tools.IsArray() { - out, _ = sjson.SetRaw(out, "tools", `[]`) - out, _ = sjson.Set(out, "tool_choice", "auto") + out, _ = sjson.SetRawBytes(out, "tools", []byte(`[]`)) + out, _ = sjson.SetBytes(out, "tool_choice", "auto") tarr := tools.Array() for i := 0; i < len(tarr); i++ { td := tarr[i] @@ -205,8 +205,8 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) farr := fns.Array() for j := 0; j < len(farr); j++ { fn := farr[j] - tool := `{}` - tool, _ = sjson.Set(tool, "type", "function") + tool := []byte(`{}`) + tool, _ = sjson.SetBytes(tool, "type", "function") if v := fn.Get("name"); v.Exists() { name := v.String() if short, ok := shortMap[name]; ok { @@ -214,32 +214,32 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } else { name = shortenNameIfNeeded(name) } - tool, _ = sjson.Set(tool, "name", name) + tool, _ = sjson.SetBytes(tool, "name", name) } if v := fn.Get("description"); v.Exists() { - tool, _ = sjson.Set(tool, "description", v.String()) + tool, _ = sjson.SetBytes(tool, "description", v.String()) } if prm := fn.Get("parameters"); prm.Exists() { // Remove optional $schema field if present - cleaned := prm.Raw - cleaned, _ = sjson.Delete(cleaned, "$schema") - cleaned, _ = sjson.Set(cleaned, "additionalProperties", false) - tool, _ = sjson.SetRaw(tool, "parameters", cleaned) + cleaned := []byte(prm.Raw) + cleaned, _ = sjson.DeleteBytes(cleaned, "$schema") + cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false) + tool, _ = sjson.SetRawBytes(tool, "parameters", cleaned) } else if prm = fn.Get("parametersJsonSchema"); prm.Exists() { // Remove optional $schema field if present - cleaned := prm.Raw - cleaned, _ = sjson.Delete(cleaned, "$schema") - cleaned, _ = sjson.Set(cleaned, "additionalProperties", false) - tool, _ = sjson.SetRaw(tool, "parameters", cleaned) + cleaned := []byte(prm.Raw) + cleaned, _ = sjson.DeleteBytes(cleaned, "$schema") + cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false) + tool, _ = sjson.SetRawBytes(tool, "parameters", cleaned) } - tool, _ = sjson.Set(tool, "strict", false) - out, _ = sjson.SetRaw(out, "tools.-1", tool) + tool, _ = sjson.SetBytes(tool, "strict", false) + out, _ = sjson.SetRawBytes(out, "tools.-1", tool) } } } // Fixed flags aligning with Codex expectations - out, _ = sjson.Set(out, "parallel_tool_calls", true) + out, _ = sjson.SetBytes(out, "parallel_tool_calls", true) // Convert Gemini thinkingConfig to Codex reasoning.effort. // Note: Google official Python SDK sends snake_case fields (thinking_level/thinking_budget). @@ -253,7 +253,7 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) if thinkingLevel.Exists() { effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String())) if effort != "" { - out, _ = sjson.Set(out, "reasoning.effort", effort) + out, _ = sjson.SetBytes(out, "reasoning.effort", effort) effortSet = true } } else { @@ -263,7 +263,7 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } if thinkingBudget.Exists() { if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok { - out, _ = sjson.Set(out, "reasoning.effort", effort) + out, _ = sjson.SetBytes(out, "reasoning.effort", effort) effortSet = true } } @@ -272,22 +272,22 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) } if !effortSet { // No thinking config, set default effort - out, _ = sjson.Set(out, "reasoning.effort", "medium") + out, _ = sjson.SetBytes(out, "reasoning.effort", "medium") } - out, _ = sjson.Set(out, "reasoning.summary", "auto") - out, _ = sjson.Set(out, "stream", true) - out, _ = sjson.Set(out, "store", false) - out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"}) + out, _ = sjson.SetBytes(out, "reasoning.summary", "auto") + out, _ = sjson.SetBytes(out, "stream", true) + out, _ = sjson.SetBytes(out, "store", false) + out, _ = sjson.SetBytes(out, "include", []string{"reasoning.encrypted_content"}) var pathsToLower []string - toolsResult := gjson.Get(out, "tools") + toolsResult := gjson.GetBytes(out, "tools") util.Walk(toolsResult, "", "type", &pathsToLower) for _, p := range pathsToLower { fullPath := fmt.Sprintf("tools.%s", p) - out, _ = sjson.Set(out, fullPath, strings.ToLower(gjson.Get(out, fullPath).String())) + out, _ = sjson.SetBytes(out, fullPath, strings.ToLower(gjson.GetBytes(out, fullPath).String())) } - return []byte(out) + return out } // shortenNameIfNeeded applies the simple shortening rule for a single name. diff --git a/internal/translator/codex/gemini/codex_gemini_response.go b/internal/translator/codex/gemini/codex_gemini_response.go index 82a2187f..4bd76791 100644 --- a/internal/translator/codex/gemini/codex_gemini_response.go +++ b/internal/translator/codex/gemini/codex_gemini_response.go @@ -7,9 +7,9 @@ package gemini import ( "bytes" "context" - "fmt" "time" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -23,7 +23,7 @@ type ConvertCodexResponseToGeminiParams struct { Model string CreatedAt int64 ResponseID string - LastStorageOutput string + LastStorageOutput []byte } // ConvertCodexResponseToGemini converts Codex streaming response format to Gemini format. @@ -38,19 +38,19 @@ type ConvertCodexResponseToGeminiParams struct { // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Gemini-compatible JSON response -func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Gemini-compatible JSON responses +func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertCodexResponseToGeminiParams{ Model: modelName, CreatedAt: 0, ResponseID: "", - LastStorageOutput: "", + LastStorageOutput: nil, } } if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) @@ -59,17 +59,17 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR typeStr := typeResult.String() // Base Gemini response template - template := `{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"gemini-2.5-pro","createTime":"2025-08-15T02:52:03.884209Z","responseId":"06CeaPH7NaCU48APvNXDyA4"}` - if (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput != "" && typeStr == "response.output_item.done" { - template = (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput + template := []byte(`{"candidates":[{"content":{"role":"model","parts":[]}}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"gemini-2.5-pro","createTime":"2025-08-15T02:52:03.884209Z","responseId":"06CeaPH7NaCU48APvNXDyA4"}`) + if len((*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput) > 0 && typeStr == "response.output_item.done" { + template = append([]byte(nil), (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput...) } else { - template, _ = sjson.Set(template, "modelVersion", (*param).(*ConvertCodexResponseToGeminiParams).Model) + template, _ = sjson.SetBytes(template, "modelVersion", (*param).(*ConvertCodexResponseToGeminiParams).Model) createdAtResult := rootResult.Get("response.created_at") if createdAtResult.Exists() { (*param).(*ConvertCodexResponseToGeminiParams).CreatedAt = createdAtResult.Int() - template, _ = sjson.Set(template, "createTime", time.Unix((*param).(*ConvertCodexResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano)) + template, _ = sjson.SetBytes(template, "createTime", time.Unix((*param).(*ConvertCodexResponseToGeminiParams).CreatedAt, 0).Format(time.RFC3339Nano)) } - template, _ = sjson.Set(template, "responseId", (*param).(*ConvertCodexResponseToGeminiParams).ResponseID) + template, _ = sjson.SetBytes(template, "responseId", (*param).(*ConvertCodexResponseToGeminiParams).ResponseID) } // Handle function call completion @@ -78,7 +78,7 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR itemType := itemResult.Get("type").String() if itemType == "function_call" { // Create function call part - functionCall := `{"functionCall":{"name":"","args":{}}}` + functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`) { // Restore original tool name if shortened n := itemResult.Get("name").String() @@ -86,7 +86,7 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR if orig, ok := rev[n]; ok { n = orig } - functionCall, _ = sjson.Set(functionCall, "functionCall.name", n) + functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", n) } // Parse and set arguments @@ -94,47 +94,48 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR if argsStr != "" { argsResult := gjson.Parse(argsStr) if argsResult.IsObject() { - functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr) + functionCall, _ = sjson.SetRawBytes(functionCall, "functionCall.args", []byte(argsStr)) } } - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", functionCall) - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", functionCall) + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") - (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput = template + (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput = append([]byte(nil), template...) // Use this return to storage message - return []string{} + return [][]byte{} } } if typeStr == "response.created" { // Handle response creation - set model and response ID - template, _ = sjson.Set(template, "modelVersion", rootResult.Get("response.model").String()) - template, _ = sjson.Set(template, "responseId", rootResult.Get("response.id").String()) + template, _ = sjson.SetBytes(template, "modelVersion", rootResult.Get("response.model").String()) + template, _ = sjson.SetBytes(template, "responseId", rootResult.Get("response.id").String()) (*param).(*ConvertCodexResponseToGeminiParams).ResponseID = rootResult.Get("response.id").String() } else if typeStr == "response.reasoning_summary_text.delta" { // Handle reasoning/thinking content delta - part := `{"thought":true,"text":""}` - part, _ = sjson.Set(part, "text", rootResult.Get("delta").String()) - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) + part := []byte(`{"thought":true,"text":""}`) + part, _ = sjson.SetBytes(part, "text", rootResult.Get("delta").String()) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) } else if typeStr == "response.output_text.delta" { // Handle regular text content delta - part := `{"text":""}` - part, _ = sjson.Set(part, "text", rootResult.Get("delta").String()) - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", rootResult.Get("delta").String()) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) } else if typeStr == "response.completed" { // Handle response completion with usage metadata - template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", rootResult.Get("response.usage.input_tokens").Int()) - template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", rootResult.Get("response.usage.output_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", rootResult.Get("response.usage.input_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", rootResult.Get("response.usage.output_tokens").Int()) totalTokens := rootResult.Get("response.usage.input_tokens").Int() + rootResult.Get("response.usage.output_tokens").Int() - template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", totalTokens) } else { - return []string{} + return [][]byte{} } - if (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput != "" { - return []string{(*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput, template} - } else { - return []string{template} + if len((*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput) > 0 { + return [][]byte{ + append([]byte(nil), (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput...), + template, + } } - + return [][]byte{template} } // ConvertCodexResponseToGeminiNonStream converts a non-streaming Codex response to a non-streaming Gemini response. @@ -149,32 +150,32 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: A Gemini-compatible JSON response containing all message content and metadata -func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Gemini-compatible JSON response containing all message content and metadata +func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { rootResult := gjson.ParseBytes(rawJSON) // Verify this is a response.completed event if rootResult.Get("type").String() != "response.completed" { - return "" + return []byte{} } // Base Gemini response template for non-streaming - template := `{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}` + template := []byte(`{"candidates":[{"content":{"role":"model","parts":[]},"finishReason":"STOP"}],"usageMetadata":{"trafficType":"PROVISIONED_THROUGHPUT"},"modelVersion":"","createTime":"","responseId":""}`) // Set model version - template, _ = sjson.Set(template, "modelVersion", modelName) + template, _ = sjson.SetBytes(template, "modelVersion", modelName) // Set response metadata from the completed response responseData := rootResult.Get("response") if responseData.Exists() { // Set response ID if responseId := responseData.Get("id"); responseId.Exists() { - template, _ = sjson.Set(template, "responseId", responseId.String()) + template, _ = sjson.SetBytes(template, "responseId", responseId.String()) } // Set creation time if createdAt := responseData.Get("created_at"); createdAt.Exists() { - template, _ = sjson.Set(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano)) + template, _ = sjson.SetBytes(template, "createTime", time.Unix(createdAt.Int(), 0).Format(time.RFC3339Nano)) } // Set usage metadata @@ -183,14 +184,14 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, outputTokens := usage.Get("output_tokens").Int() totalTokens := inputTokens + outputTokens - template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens) - template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens) - template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", inputTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", outputTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", totalTokens) } // Process output content to build parts array hasToolCall := false - var pendingFunctionCalls []string + var pendingFunctionCalls [][]byte flushPendingFunctionCalls := func() { if len(pendingFunctionCalls) == 0 { @@ -199,7 +200,7 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, // Add all pending function calls as individual parts // This maintains the original Gemini API format while ensuring consecutive calls are grouped together for _, fc := range pendingFunctionCalls { - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", fc) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", fc) } pendingFunctionCalls = nil } @@ -215,9 +216,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, // Add thinking content if content := value.Get("content"); content.Exists() { - part := `{"text":"","thought":true}` - part, _ = sjson.Set(part, "text", content.String()) - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) + part := []byte(`{"text":"","thought":true}`) + part, _ = sjson.SetBytes(part, "text", content.String()) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) } case "message": @@ -229,9 +230,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, content.ForEach(func(_, contentItem gjson.Result) bool { if contentItem.Get("type").String() == "output_text" { if text := contentItem.Get("text"); text.Exists() { - part := `{"text":""}` - part, _ = sjson.Set(part, "text", text.String()) - template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", text.String()) + template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part) } } return true @@ -241,21 +242,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, case "function_call": // Collect function call for potential merging with consecutive ones hasToolCall = true - functionCall := `{"functionCall":{"args":{},"name":""}}` + functionCall := []byte(`{"functionCall":{"args":{},"name":""}}`) { n := value.Get("name").String() rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON) if orig, ok := rev[n]; ok { n = orig } - functionCall, _ = sjson.Set(functionCall, "functionCall.name", n) + functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", n) } // Parse and set arguments if argsStr := value.Get("arguments").String(); argsStr != "" { argsResult := gjson.Parse(argsStr) if argsResult.IsObject() { - functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsStr) + functionCall, _ = sjson.SetRawBytes(functionCall, "functionCall.args", []byte(argsStr)) } } @@ -270,9 +271,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, // Set finish reason based on whether there were tool calls if hasToolCall { - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") } else { - template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP") + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP") } } return template @@ -307,6 +308,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string { return rev } -func GeminiTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_request.go b/internal/translator/codex/openai/chat-completions/codex_openai_request.go index 6941ec46..6cc701e7 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_request.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_request.go @@ -29,42 +29,42 @@ import ( func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := inputRawJSON // Start with empty JSON object - out := `{"instructions":""}` + out := []byte(`{"instructions":""}`) // Stream must be set to true - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // Codex not support temperature, top_p, top_k, max_output_tokens, so comment them // if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() { - // out, _ = sjson.Set(out, "temperature", v.Value()) + // out, _ = sjson.SetBytes(out, "temperature", v.Value()) // } // if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() { - // out, _ = sjson.Set(out, "top_p", v.Value()) + // out, _ = sjson.SetBytes(out, "top_p", v.Value()) // } // if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() { - // out, _ = sjson.Set(out, "top_k", v.Value()) + // out, _ = sjson.SetBytes(out, "top_k", v.Value()) // } // Map token limits // if v := gjson.GetBytes(rawJSON, "max_tokens"); v.Exists() { - // out, _ = sjson.Set(out, "max_output_tokens", v.Value()) + // out, _ = sjson.SetBytes(out, "max_output_tokens", v.Value()) // } // if v := gjson.GetBytes(rawJSON, "max_completion_tokens"); v.Exists() { - // out, _ = sjson.Set(out, "max_output_tokens", v.Value()) + // out, _ = sjson.SetBytes(out, "max_output_tokens", v.Value()) // } // Map reasoning effort if v := gjson.GetBytes(rawJSON, "reasoning_effort"); v.Exists() { - out, _ = sjson.Set(out, "reasoning.effort", v.Value()) + out, _ = sjson.SetBytes(out, "reasoning.effort", v.Value()) } else { - out, _ = sjson.Set(out, "reasoning.effort", "medium") + out, _ = sjson.SetBytes(out, "reasoning.effort", "medium") } - out, _ = sjson.Set(out, "parallel_tool_calls", true) - out, _ = sjson.Set(out, "reasoning.summary", "auto") - out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"}) + out, _ = sjson.SetBytes(out, "parallel_tool_calls", true) + out, _ = sjson.SetBytes(out, "reasoning.summary", "auto") + out, _ = sjson.SetBytes(out, "include", []string{"reasoning.encrypted_content"}) // Model - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Build tool name shortening map from original tools (if any) originalToolNameMap := map[string]string{} @@ -100,9 +100,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // if m.Get("role").String() == "system" { // c := m.Get("content") // if c.Type == gjson.String { - // out, _ = sjson.Set(out, "instructions", c.String()) + // out, _ = sjson.SetBytes(out, "instructions", c.String()) // } else if c.IsObject() && c.Get("type").String() == "text" { - // out, _ = sjson.Set(out, "instructions", c.Get("text").String()) + // out, _ = sjson.SetBytes(out, "instructions", c.Get("text").String()) // } // break // } @@ -110,7 +110,7 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // } // Build input from messages, handling all message types including tool calls - out, _ = sjson.SetRaw(out, "input", `[]`) + out, _ = sjson.SetRawBytes(out, "input", []byte(`[]`)) if messages.IsArray() { arr := messages.Array() for i := 0; i < len(arr); i++ { @@ -124,23 +124,23 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b content := m.Get("content").String() // Create function_call_output object - funcOutput := `{}` - funcOutput, _ = sjson.Set(funcOutput, "type", "function_call_output") - funcOutput, _ = sjson.Set(funcOutput, "call_id", toolCallID) - funcOutput, _ = sjson.Set(funcOutput, "output", content) - out, _ = sjson.SetRaw(out, "input.-1", funcOutput) + funcOutput := []byte(`{}`) + funcOutput, _ = sjson.SetBytes(funcOutput, "type", "function_call_output") + funcOutput, _ = sjson.SetBytes(funcOutput, "call_id", toolCallID) + funcOutput, _ = sjson.SetBytes(funcOutput, "output", content) + out, _ = sjson.SetRawBytes(out, "input.-1", funcOutput) default: // Handle regular messages - msg := `{}` - msg, _ = sjson.Set(msg, "type", "message") + msg := []byte(`{}`) + msg, _ = sjson.SetBytes(msg, "type", "message") if role == "system" { - msg, _ = sjson.Set(msg, "role", "developer") + msg, _ = sjson.SetBytes(msg, "role", "developer") } else { - msg, _ = sjson.Set(msg, "role", role) + msg, _ = sjson.SetBytes(msg, "role", role) } - msg, _ = sjson.SetRaw(msg, "content", `[]`) + msg, _ = sjson.SetRawBytes(msg, "content", []byte(`[]`)) // Handle regular content c := m.Get("content") @@ -150,10 +150,10 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b if role == "assistant" { partType = "output_text" } - part := `{}` - part, _ = sjson.Set(part, "type", partType) - part, _ = sjson.Set(part, "text", c.String()) - msg, _ = sjson.SetRaw(msg, "content.-1", part) + part := []byte(`{}`) + part, _ = sjson.SetBytes(part, "type", partType) + part, _ = sjson.SetBytes(part, "text", c.String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) } else if c.Exists() && c.IsArray() { items := c.Array() for j := 0; j < len(items); j++ { @@ -165,32 +165,32 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b if role == "assistant" { partType = "output_text" } - part := `{}` - part, _ = sjson.Set(part, "type", partType) - part, _ = sjson.Set(part, "text", it.Get("text").String()) - msg, _ = sjson.SetRaw(msg, "content.-1", part) + part := []byte(`{}`) + part, _ = sjson.SetBytes(part, "type", partType) + part, _ = sjson.SetBytes(part, "text", it.Get("text").String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) case "image_url": // Map image inputs to input_image for Responses API if role == "user" { - part := `{}` - part, _ = sjson.Set(part, "type", "input_image") + part := []byte(`{}`) + part, _ = sjson.SetBytes(part, "type", "input_image") if u := it.Get("image_url.url"); u.Exists() { - part, _ = sjson.Set(part, "image_url", u.String()) + part, _ = sjson.SetBytes(part, "image_url", u.String()) } - msg, _ = sjson.SetRaw(msg, "content.-1", part) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) } case "file": if role == "user" { fileData := it.Get("file.file_data").String() filename := it.Get("file.filename").String() if fileData != "" { - part := `{}` - part, _ = sjson.Set(part, "type", "input_file") - part, _ = sjson.Set(part, "file_data", fileData) + part := []byte(`{}`) + part, _ = sjson.SetBytes(part, "type", "input_file") + part, _ = sjson.SetBytes(part, "file_data", fileData) if filename != "" { - part, _ = sjson.Set(part, "filename", filename) + part, _ = sjson.SetBytes(part, "filename", filename) } - msg, _ = sjson.SetRaw(msg, "content.-1", part) + msg, _ = sjson.SetRawBytes(msg, "content.-1", part) } } } @@ -200,8 +200,8 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // Don't emit empty assistant messages when only tool_calls // are present — Responses API needs function_call items // directly, otherwise call_id matching fails (#2132). - if role != "assistant" || len(gjson.Get(msg, "content").Array()) > 0 { - out, _ = sjson.SetRaw(out, "input.-1", msg) + if role != "assistant" || len(gjson.GetBytes(msg, "content").Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "input.-1", msg) } // Handle tool calls for assistant messages as separate top-level objects @@ -213,9 +213,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b tc := toolCallsArr[j] if tc.Get("type").String() == "function" { // Create function_call as top-level object - funcCall := `{}` - funcCall, _ = sjson.Set(funcCall, "type", "function_call") - funcCall, _ = sjson.Set(funcCall, "call_id", tc.Get("id").String()) + funcCall := []byte(`{}`) + funcCall, _ = sjson.SetBytes(funcCall, "type", "function_call") + funcCall, _ = sjson.SetBytes(funcCall, "call_id", tc.Get("id").String()) { name := tc.Get("function.name").String() if short, ok := originalToolNameMap[name]; ok { @@ -223,10 +223,10 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b } else { name = shortenNameIfNeeded(name) } - funcCall, _ = sjson.Set(funcCall, "name", name) + funcCall, _ = sjson.SetBytes(funcCall, "name", name) } - funcCall, _ = sjson.Set(funcCall, "arguments", tc.Get("function.arguments").String()) - out, _ = sjson.SetRaw(out, "input.-1", funcCall) + funcCall, _ = sjson.SetBytes(funcCall, "arguments", tc.Get("function.arguments").String()) + out, _ = sjson.SetRawBytes(out, "input.-1", funcCall) } } } @@ -240,26 +240,26 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b text := gjson.GetBytes(rawJSON, "text") if rf.Exists() { // Always create text object when response_format provided - if !gjson.Get(out, "text").Exists() { - out, _ = sjson.SetRaw(out, "text", `{}`) + if !gjson.GetBytes(out, "text").Exists() { + out, _ = sjson.SetRawBytes(out, "text", []byte(`{}`)) } rft := rf.Get("type").String() switch rft { case "text": - out, _ = sjson.Set(out, "text.format.type", "text") + out, _ = sjson.SetBytes(out, "text.format.type", "text") case "json_schema": js := rf.Get("json_schema") if js.Exists() { - out, _ = sjson.Set(out, "text.format.type", "json_schema") + out, _ = sjson.SetBytes(out, "text.format.type", "json_schema") if v := js.Get("name"); v.Exists() { - out, _ = sjson.Set(out, "text.format.name", v.Value()) + out, _ = sjson.SetBytes(out, "text.format.name", v.Value()) } if v := js.Get("strict"); v.Exists() { - out, _ = sjson.Set(out, "text.format.strict", v.Value()) + out, _ = sjson.SetBytes(out, "text.format.strict", v.Value()) } if v := js.Get("schema"); v.Exists() { - out, _ = sjson.SetRaw(out, "text.format.schema", v.Raw) + out, _ = sjson.SetRawBytes(out, "text.format.schema", []byte(v.Raw)) } } } @@ -267,23 +267,23 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // Map verbosity if provided if text.Exists() { if v := text.Get("verbosity"); v.Exists() { - out, _ = sjson.Set(out, "text.verbosity", v.Value()) + out, _ = sjson.SetBytes(out, "text.verbosity", v.Value()) } } } else if text.Exists() { // If only text.verbosity present (no response_format), map verbosity if v := text.Get("verbosity"); v.Exists() { - if !gjson.Get(out, "text").Exists() { - out, _ = sjson.SetRaw(out, "text", `{}`) + if !gjson.GetBytes(out, "text").Exists() { + out, _ = sjson.SetRawBytes(out, "text", []byte(`{}`)) } - out, _ = sjson.Set(out, "text.verbosity", v.Value()) + out, _ = sjson.SetBytes(out, "text.verbosity", v.Value()) } } // Map tools (flatten function fields) tools := gjson.GetBytes(rawJSON, "tools") if tools.IsArray() && len(tools.Array()) > 0 { - out, _ = sjson.SetRaw(out, "tools", `[]`) + out, _ = sjson.SetRawBytes(out, "tools", []byte(`[]`)) arr := tools.Array() for i := 0; i < len(arr); i++ { t := arr[i] @@ -291,13 +291,13 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b // Pass through built-in tools (e.g. {"type":"web_search"}) directly for the Responses API. // Only "function" needs structural conversion because Chat Completions nests details under "function". if toolType != "" && toolType != "function" && t.IsObject() { - out, _ = sjson.SetRaw(out, "tools.-1", t.Raw) + out, _ = sjson.SetRawBytes(out, "tools.-1", []byte(t.Raw)) continue } if toolType == "function" { - item := `{}` - item, _ = sjson.Set(item, "type", "function") + item := []byte(`{}`) + item, _ = sjson.SetBytes(item, "type", "function") fn := t.Get("function") if fn.Exists() { if v := fn.Get("name"); v.Exists() { @@ -307,19 +307,19 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b } else { name = shortenNameIfNeeded(name) } - item, _ = sjson.Set(item, "name", name) + item, _ = sjson.SetBytes(item, "name", name) } if v := fn.Get("description"); v.Exists() { - item, _ = sjson.Set(item, "description", v.Value()) + item, _ = sjson.SetBytes(item, "description", v.Value()) } if v := fn.Get("parameters"); v.Exists() { - item, _ = sjson.SetRaw(item, "parameters", v.Raw) + item, _ = sjson.SetRawBytes(item, "parameters", []byte(v.Raw)) } if v := fn.Get("strict"); v.Exists() { - item, _ = sjson.Set(item, "strict", v.Value()) + item, _ = sjson.SetBytes(item, "strict", v.Value()) } } - out, _ = sjson.SetRaw(out, "tools.-1", item) + out, _ = sjson.SetRawBytes(out, "tools.-1", item) } } } @@ -330,7 +330,7 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b if tc := gjson.GetBytes(rawJSON, "tool_choice"); tc.Exists() { switch { case tc.Type == gjson.String: - out, _ = sjson.Set(out, "tool_choice", tc.String()) + out, _ = sjson.SetBytes(out, "tool_choice", tc.String()) case tc.IsObject(): tcType := tc.Get("type").String() if tcType == "function" { @@ -342,21 +342,21 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b name = shortenNameIfNeeded(name) } } - choice := `{}` - choice, _ = sjson.Set(choice, "type", "function") + choice := []byte(`{}`) + choice, _ = sjson.SetBytes(choice, "type", "function") if name != "" { - choice, _ = sjson.Set(choice, "name", name) + choice, _ = sjson.SetBytes(choice, "name", name) } - out, _ = sjson.SetRaw(out, "tool_choice", choice) + out, _ = sjson.SetRawBytes(out, "tool_choice", choice) } else if tcType != "" { // Built-in tool choices (e.g. {"type":"web_search"}) are already Responses-compatible. - out, _ = sjson.SetRaw(out, "tool_choice", tc.Raw) + out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(tc.Raw)) } } } - out, _ = sjson.Set(out, "store", false) - return []byte(out) + out, _ = sjson.SetBytes(out, "store", false) + return out } // shortenNameIfNeeded applies the simple shortening rule for a single name. diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_response.go b/internal/translator/codex/openai/chat-completions/codex_openai_response.go index 0054d995..94367e50 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_response.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_response.go @@ -41,8 +41,8 @@ type ConvertCliToOpenAIParams struct { // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing an OpenAI-compatible JSON response -func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of OpenAI-compatible JSON responses +func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertCliToOpenAIParams{ Model: modelName, @@ -55,12 +55,12 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR } if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) // Initialize the OpenAI SSE template. - template := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}` + template := []byte(`{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`) rootResult := gjson.ParseBytes(rawJSON) @@ -70,67 +70,67 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR (*param).(*ConvertCliToOpenAIParams).ResponseID = rootResult.Get("response.id").String() (*param).(*ConvertCliToOpenAIParams).CreatedAt = rootResult.Get("response.created_at").Int() (*param).(*ConvertCliToOpenAIParams).Model = rootResult.Get("response.model").String() - return []string{} + return [][]byte{} } // Extract and set the model version. cachedModel := (*param).(*ConvertCliToOpenAIParams).Model if modelResult := gjson.GetBytes(rawJSON, "model"); modelResult.Exists() { - template, _ = sjson.Set(template, "model", modelResult.String()) + template, _ = sjson.SetBytes(template, "model", modelResult.String()) } else if cachedModel != "" { - template, _ = sjson.Set(template, "model", cachedModel) + template, _ = sjson.SetBytes(template, "model", cachedModel) } else if modelName != "" { - template, _ = sjson.Set(template, "model", modelName) + template, _ = sjson.SetBytes(template, "model", modelName) } - template, _ = sjson.Set(template, "created", (*param).(*ConvertCliToOpenAIParams).CreatedAt) + template, _ = sjson.SetBytes(template, "created", (*param).(*ConvertCliToOpenAIParams).CreatedAt) // Extract and set the response ID. - template, _ = sjson.Set(template, "id", (*param).(*ConvertCliToOpenAIParams).ResponseID) + template, _ = sjson.SetBytes(template, "id", (*param).(*ConvertCliToOpenAIParams).ResponseID) // Extract and set usage metadata (token counts). if usageResult := gjson.GetBytes(rawJSON, "response.usage"); usageResult.Exists() { if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens", outputTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens", outputTokensResult.Int()) } if totalTokensResult := usageResult.Get("total_tokens"); totalTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.total_tokens", totalTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.total_tokens", totalTokensResult.Int()) } if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens", inputTokensResult.Int()) } if cachedTokensResult := usageResult.Get("input_tokens_details.cached_tokens"); cachedTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cachedTokensResult.Int()) } if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int()) } } if dataType == "response.reasoning_summary_text.delta" { if deltaResult := rootResult.Get("delta"); deltaResult.Exists() { - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", deltaResult.String()) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", deltaResult.String()) } } else if dataType == "response.reasoning_summary_text.done" { - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", "\n\n") + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", "\n\n") } else if dataType == "response.output_text.delta" { if deltaResult := rootResult.Get("delta"); deltaResult.Exists() { - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.Set(template, "choices.0.delta.content", deltaResult.String()) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.content", deltaResult.String()) } } else if dataType == "response.completed" { finishReason := "stop" if (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex != -1 { finishReason = "tool_calls" } - template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason) - template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", finishReason) } else if dataType == "response.output_item.added" { itemResult := rootResult.Get("item") if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" { - return []string{} + return [][]byte{} } // Increment index for this new function call item. @@ -138,9 +138,9 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR (*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta = false (*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced = true - functionCallItemTemplate := `{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}` - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", itemResult.Get("call_id").String()) + functionCallItemTemplate := []byte(`{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}`) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", itemResult.Get("call_id").String()) // Restore original tool name if it was shortened. name := itemResult.Get("name").String() @@ -148,59 +148,59 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR if orig, ok := rev[name]; ok { name = orig } - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", name) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", "") + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.name", name) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", "") - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) } else if dataType == "response.function_call_arguments.delta" { (*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta = true deltaValue := rootResult.Get("delta").String() - functionCallItemTemplate := `{"index":0,"function":{"arguments":""}}` - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", deltaValue) + functionCallItemTemplate := []byte(`{"index":0,"function":{"arguments":""}}`) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", deltaValue) - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) } else if dataType == "response.function_call_arguments.done" { if (*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta { // Arguments were already streamed via delta events; nothing to emit. - return []string{} + return [][]byte{} } // Fallback: no delta events were received, emit the full arguments as a single chunk. fullArgs := rootResult.Get("arguments").String() - functionCallItemTemplate := `{"index":0,"function":{"arguments":""}}` - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fullArgs) + functionCallItemTemplate := []byte(`{"index":0,"function":{"arguments":""}}`) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", fullArgs) - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) } else if dataType == "response.output_item.done" { itemResult := rootResult.Get("item") if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" { - return []string{} + return [][]byte{} } if (*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced { // Tool call was already announced via output_item.added; skip emission. (*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced = false - return []string{} + return [][]byte{} } // Fallback path: model skipped output_item.added, so emit complete tool call now. (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex++ - functionCallItemTemplate := `{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}` - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) + functionCallItemTemplate := []byte(`{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}`) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex) - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", itemResult.Get("call_id").String()) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", itemResult.Get("call_id").String()) // Restore original tool name if it was shortened. name := itemResult.Get("name").String() @@ -208,17 +208,17 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR if orig, ok := rev[name]; ok { name = orig } - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", name) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.name", name) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", itemResult.Get("arguments").String()) - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", itemResult.Get("arguments").String()) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate) } else { - return []string{} + return [][]byte{} } - return []string{template} + return [][]byte{template} } // ConvertCodexResponseToOpenAINonStream converts a non-streaming Codex response to a non-streaming OpenAI response. @@ -233,53 +233,53 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: An OpenAI-compatible JSON response containing all message content and metadata -func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: An OpenAI-compatible JSON response containing all message content and metadata +func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { rootResult := gjson.ParseBytes(rawJSON) // Verify this is a response.completed event if rootResult.Get("type").String() != "response.completed" { - return "" + return []byte{} } unixTimestamp := time.Now().Unix() responseResult := rootResult.Get("response") - template := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}` + template := []byte(`{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`) // Extract and set the model version. if modelResult := responseResult.Get("model"); modelResult.Exists() { - template, _ = sjson.Set(template, "model", modelResult.String()) + template, _ = sjson.SetBytes(template, "model", modelResult.String()) } // Extract and set the creation timestamp. if createdAtResult := responseResult.Get("created_at"); createdAtResult.Exists() { - template, _ = sjson.Set(template, "created", createdAtResult.Int()) + template, _ = sjson.SetBytes(template, "created", createdAtResult.Int()) } else { - template, _ = sjson.Set(template, "created", unixTimestamp) + template, _ = sjson.SetBytes(template, "created", unixTimestamp) } // Extract and set the response ID. if idResult := responseResult.Get("id"); idResult.Exists() { - template, _ = sjson.Set(template, "id", idResult.String()) + template, _ = sjson.SetBytes(template, "id", idResult.String()) } // Extract and set usage metadata (token counts). if usageResult := responseResult.Get("usage"); usageResult.Exists() { if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens", outputTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens", outputTokensResult.Int()) } if totalTokensResult := usageResult.Get("total_tokens"); totalTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.total_tokens", totalTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.total_tokens", totalTokensResult.Int()) } if inputTokensResult := usageResult.Get("input_tokens"); inputTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens", inputTokensResult.Int()) } if cachedTokensResult := usageResult.Get("input_tokens_details.cached_tokens"); cachedTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cachedTokensResult.Int()) } if reasoningTokensResult := usageResult.Get("output_tokens_details.reasoning_tokens"); reasoningTokensResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens_details.reasoning_tokens", reasoningTokensResult.Int()) } } @@ -289,7 +289,7 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original outputArray := outputResult.Array() var contentText string var reasoningText string - var toolCalls []string + var toolCalls [][]byte for _, outputItem := range outputArray { outputType := outputItem.Get("type").String() @@ -319,10 +319,10 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original } case "function_call": // Handle function call content - functionCallTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}` + functionCallTemplate := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`) if callIdResult := outputItem.Get("call_id"); callIdResult.Exists() { - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", callIdResult.String()) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", callIdResult.String()) } if nameResult := outputItem.Get("name"); nameResult.Exists() { @@ -331,11 +331,11 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original if orig, ok := rev[n]; ok { n = orig } - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", n) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", n) } if argsResult := outputItem.Get("arguments"); argsResult.Exists() { - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", argsResult.String()) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.arguments", argsResult.String()) } toolCalls = append(toolCalls, functionCallTemplate) @@ -344,22 +344,22 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original // Set content and reasoning content if found if contentText != "" { - template, _ = sjson.Set(template, "choices.0.message.content", contentText) - template, _ = sjson.Set(template, "choices.0.message.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.message.content", contentText) + template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant") } if reasoningText != "" { - template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningText) - template, _ = sjson.Set(template, "choices.0.message.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.message.reasoning_content", reasoningText) + template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant") } // Add tool calls if any if len(toolCalls) > 0 { - template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.message.tool_calls", []byte(`[]`)) for _, toolCall := range toolCalls { - template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", toolCall) + template, _ = sjson.SetRawBytes(template, "choices.0.message.tool_calls.-1", toolCall) } - template, _ = sjson.Set(template, "choices.0.message.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant") } } @@ -367,8 +367,8 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original if statusResult := responseResult.Get("status"); statusResult.Exists() { status := statusResult.String() if status == "completed" { - template, _ = sjson.Set(template, "choices.0.finish_reason", "stop") - template, _ = sjson.Set(template, "choices.0.native_finish_reason", "stop") + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", "stop") + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", "stop") } } diff --git a/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go b/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go index 70aaea06..06e917d3 100644 --- a/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go +++ b/internal/translator/codex/openai/chat-completions/codex_openai_response_test.go @@ -23,7 +23,7 @@ func TestConvertCodexResponseToOpenAI_StreamSetsModelFromResponseCreated(t *test t.Fatalf("expected 1 chunk, got %d", len(out)) } - gotModel := gjson.Get(out[0], "model").String() + gotModel := gjson.GetBytes(out[0], "model").String() if gotModel != modelName { t.Fatalf("expected model %q, got %q", modelName, gotModel) } @@ -40,7 +40,7 @@ func TestConvertCodexResponseToOpenAI_FirstChunkUsesRequestModelName(t *testing. t.Fatalf("expected 1 chunk, got %d", len(out)) } - gotModel := gjson.Get(out[0], "model").String() + gotModel := gjson.GetBytes(out[0], "model").String() if gotModel != modelName { t.Fatalf("expected model %q, got %q", modelName, gotModel) } diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request.go b/internal/translator/codex/openai/responses/codex_openai-responses_request.go index 360c037f..b16877b7 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -12,8 +12,8 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, inputResult := gjson.GetBytes(rawJSON, "input") if inputResult.Type == gjson.String { - input, _ := sjson.Set(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`, "0.content.0.text", inputResult.String()) - rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", []byte(input)) + input, _ := sjson.SetBytes([]byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`), "0.content.0.text", inputResult.String()) + rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", input) } rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true) diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_response.go b/internal/translator/codex/openai/responses/codex_openai-responses_response.go index e84b817b..968c1163 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_response.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_response.go @@ -3,7 +3,6 @@ package responses import ( "bytes" "context" - "fmt" "github.com/tidwall/gjson" ) @@ -11,23 +10,25 @@ import ( // ConvertCodexResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks // to OpenAI Responses SSE events (response.*). -func ConvertCodexResponseToOpenAIResponses(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) []string { +func ConvertCodexResponseToOpenAIResponses(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) [][]byte { if bytes.HasPrefix(rawJSON, []byte("data:")) { rawJSON = bytes.TrimSpace(rawJSON[5:]) - out := fmt.Sprintf("data: %s", string(rawJSON)) - return []string{out} + out := make([]byte, 0, len(rawJSON)+len("data: ")) + out = append(out, []byte("data: ")...) + out = append(out, rawJSON...) + return [][]byte{out} } - return []string{string(rawJSON)} + return [][]byte{rawJSON} } // ConvertCodexResponseToOpenAIResponsesNonStream builds a single Responses JSON // from a non-streaming OpenAI Chat Completions response. -func ConvertCodexResponseToOpenAIResponsesNonStream(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) string { +func ConvertCodexResponseToOpenAIResponsesNonStream(_ context.Context, _ string, _, _, rawJSON []byte, _ *any) []byte { rootResult := gjson.ParseBytes(rawJSON) // Verify this is a response.completed event if rootResult.Get("type").String() != "response.completed" { - return "" + return []byte{} } responseResult := rootResult.Get("response") - return responseResult.Raw + return []byte(responseResult.Raw) } diff --git a/internal/translator/common/bytes.go b/internal/translator/common/bytes.go new file mode 100644 index 00000000..ff42d7e9 --- /dev/null +++ b/internal/translator/common/bytes.go @@ -0,0 +1,67 @@ +package common + +import ( + "strconv" + + "github.com/tidwall/sjson" +) + +func WrapGeminiCLIResponse(response []byte) []byte { + out, err := sjson.SetRawBytes([]byte(`{"response":{}}`), "response", response) + if err != nil { + return response + } + return out +} + +func GeminiTokenCountJSON(count int64) []byte { + out := make([]byte, 0, 96) + out = append(out, `{"totalTokens":`...) + out = strconv.AppendInt(out, count, 10) + out = append(out, `,"promptTokensDetails":[{"modality":"TEXT","tokenCount":`...) + out = strconv.AppendInt(out, count, 10) + out = append(out, `}]}`...) + return out +} + +func ClaudeInputTokensJSON(count int64) []byte { + out := make([]byte, 0, 32) + out = append(out, `{"input_tokens":`...) + out = strconv.AppendInt(out, count, 10) + out = append(out, '}') + return out +} + +func SSEEventData(event string, payload []byte) []byte { + out := make([]byte, 0, len(event)+len(payload)+14) + out = append(out, "event: "...) + out = append(out, event...) + out = append(out, '\n') + out = append(out, "data: "...) + out = append(out, payload...) + return out +} + +func AppendSSEEventString(out []byte, event, payload string, trailingNewlines int) []byte { + out = append(out, "event: "...) + out = append(out, event...) + out = append(out, '\n') + out = append(out, "data: "...) + out = append(out, payload...) + for i := 0; i < trailingNewlines; i++ { + out = append(out, '\n') + } + return out +} + +func AppendSSEEventBytes(out []byte, event string, payload []byte, trailingNewlines int) []byte { + out = append(out, "event: "...) + out = append(out, event...) + out = append(out, '\n') + out = append(out, "data: "...) + out = append(out, payload...) + for i := 0; i < trailingNewlines; i++ { + out = append(out, '\n') + } + return out +} diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go index 18ce4495..d2567a03 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_request.go @@ -38,30 +38,30 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] rawJSON := inputRawJSON // Build output Gemini CLI request JSON - out := `{"model":"","request":{"contents":[]}}` - out, _ = sjson.Set(out, "model", modelName) + out := []byte(`{"model":"","request":{"contents":[]}}`) + out, _ = sjson.SetBytes(out, "model", modelName) // system instruction if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() { - systemInstruction := `{"role":"user","parts":[]}` + systemInstruction := []byte(`{"role":"user","parts":[]}`) hasSystemParts := false systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool { if systemPromptResult.Get("type").String() == "text" { textResult := systemPromptResult.Get("text") if textResult.Type == gjson.String { - part := `{"text":""}` - part, _ = sjson.Set(part, "text", textResult.String()) - systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", textResult.String()) + systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part) hasSystemParts = true } } return true }) if hasSystemParts { - out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction) + out, _ = sjson.SetRawBytes(out, "request.systemInstruction", systemInstruction) } } else if systemResult.Type == gjson.String { - out, _ = sjson.Set(out, "request.systemInstruction.parts.-1.text", systemResult.String()) + out, _ = sjson.SetBytes(out, "request.systemInstruction.parts.-1.text", systemResult.String()) } // contents @@ -76,28 +76,28 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] role = "model" } - contentJSON := `{"role":"","parts":[]}` - contentJSON, _ = sjson.Set(contentJSON, "role", role) + contentJSON := []byte(`{"role":"","parts":[]}`) + contentJSON, _ = sjson.SetBytes(contentJSON, "role", role) contentsResult := messageResult.Get("content") if contentsResult.IsArray() { contentsResult.ForEach(func(_, contentResult gjson.Result) bool { switch contentResult.Get("type").String() { case "text": - part := `{"text":""}` - part, _ = sjson.Set(part, "text", contentResult.Get("text").String()) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", contentResult.Get("text").String()) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) case "tool_use": functionName := contentResult.Get("name").String() functionArgs := contentResult.Get("input").String() argsResult := gjson.Parse(functionArgs) if argsResult.IsObject() && gjson.Valid(functionArgs) { - part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}` - part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature) - part, _ = sjson.Set(part, "functionCall.name", functionName) - part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`) + part, _ = sjson.SetBytes(part, "thoughtSignature", geminiCLIClaudeThoughtSignature) + part, _ = sjson.SetBytes(part, "functionCall.name", functionName) + part, _ = sjson.SetRawBytes(part, "functionCall.args", []byte(functionArgs)) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) } case "tool_result": @@ -111,10 +111,10 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-") } responseData := contentResult.Get("content").Raw - part := `{"functionResponse":{"name":"","response":{"result":""}}}` - part, _ = sjson.Set(part, "functionResponse.name", funcName) - part, _ = sjson.Set(part, "functionResponse.response.result", responseData) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`) + part, _ = sjson.SetBytes(part, "functionResponse.name", funcName) + part, _ = sjson.SetBytes(part, "functionResponse.response.result", responseData) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) case "image": source := contentResult.Get("source") @@ -122,21 +122,21 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] mimeType := source.Get("media_type").String() data := source.Get("data").String() if mimeType != "" && data != "" { - part := `{"inlineData":{"mime_type":"","data":""}}` - part, _ = sjson.Set(part, "inlineData.mime_type", mimeType) - part, _ = sjson.Set(part, "inlineData.data", data) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"inlineData":{"mime_type":"","data":""}}`) + part, _ = sjson.SetBytes(part, "inlineData.mime_type", mimeType) + part, _ = sjson.SetBytes(part, "inlineData.data", data) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) } } } return true }) - out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON) + out, _ = sjson.SetRawBytes(out, "request.contents.-1", contentJSON) } else if contentsResult.Type == gjson.String { - part := `{"text":""}` - part, _ = sjson.Set(part, "text", contentsResult.String()) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) - out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", contentsResult.String()) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) + out, _ = sjson.SetRawBytes(out, "request.contents.-1", contentJSON) } return true }) @@ -149,26 +149,26 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] inputSchemaResult := toolResult.Get("input_schema") if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { inputSchema := util.CleanJSONSchemaForGemini(inputSchemaResult.Raw) - tool, _ := sjson.Delete(toolResult.Raw, "input_schema") - tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) - tool, _ = sjson.Delete(tool, "strict") - tool, _ = sjson.Delete(tool, "input_examples") - tool, _ = sjson.Delete(tool, "type") - tool, _ = sjson.Delete(tool, "cache_control") - tool, _ = sjson.Delete(tool, "defer_loading") - tool, _ = sjson.Delete(tool, "eager_input_streaming") - if gjson.Valid(tool) && gjson.Parse(tool).IsObject() { + tool, _ := sjson.DeleteBytes([]byte(toolResult.Raw), "input_schema") + tool, _ = sjson.SetRawBytes(tool, "parametersJsonSchema", []byte(inputSchema)) + tool, _ = sjson.DeleteBytes(tool, "strict") + tool, _ = sjson.DeleteBytes(tool, "input_examples") + tool, _ = sjson.DeleteBytes(tool, "type") + tool, _ = sjson.DeleteBytes(tool, "cache_control") + tool, _ = sjson.DeleteBytes(tool, "defer_loading") + tool, _ = sjson.DeleteBytes(tool, "eager_input_streaming") + if gjson.ValidBytes(tool) && gjson.ParseBytes(tool).IsObject() { if !hasTools { - out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`) + out, _ = sjson.SetRawBytes(out, "request.tools", []byte(`[{"functionDeclarations":[]}]`)) hasTools = true } - out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool) + out, _ = sjson.SetRawBytes(out, "request.tools.0.functionDeclarations.-1", tool) } } return true }) if !hasTools { - out, _ = sjson.Delete(out, "request.tools") + out, _ = sjson.DeleteBytes(out, "request.tools") } } @@ -186,15 +186,15 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] switch toolChoiceType { case "auto": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "AUTO") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "AUTO") case "none": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "NONE") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "NONE") case "any": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY") case "tool": - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY") + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY") if toolChoiceName != "" { - out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) + out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) } } } @@ -206,8 +206,8 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] case "enabled": if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } case "adaptive", "auto": // For adaptive thinking: @@ -219,25 +219,23 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) [] effort = strings.ToLower(strings.TrimSpace(v.String())) } if effort != "" { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort) } else { - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", "high") } - out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true) + out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true) } } if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.temperature", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.temperature", v.Num) } if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.topP", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.topP", v.Num) } if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "request.generationConfig.topK", v.Num) + out, _ = sjson.SetBytes(out, "request.generationConfig.topK", v.Num) } - outBytes := []byte(out) - outBytes = common.AttachDefaultSafetySettings(outBytes, "request.safetySettings") - - return outBytes + out = common.AttachDefaultSafetySettings(out, "request.safetySettings") + return out } diff --git a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go index 3d310d8b..b5809632 100644 --- a/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go +++ b/internal/translator/gemini-cli/claude/gemini-cli_claude_response.go @@ -14,6 +14,7 @@ import ( "sync/atomic" "time" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -47,8 +48,8 @@ var toolUseIDCounter uint64 // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing a Claude Code-compatible JSON response -func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of bytes, each containing a Claude Code-compatible SSE payload. +func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &Params{ HasFirstResponse: false, @@ -60,34 +61,33 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque if bytes.Equal(rawJSON, []byte("[DONE]")) { // Only send message_stop if we have actually output content if (*param).(*Params).HasContent { - return []string{ - "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n", - } + return [][]byte{translatorcommon.AppendSSEEventString(nil, "message_stop", `{"type":"message_stop"}`, 3)} } - return []string{} + return [][]byte{} } // Track whether tools are being used in this response chunk usedTool := false - output := "" + output := make([]byte, 0, 1024) + appendEvent := func(event, payload string) { + output = translatorcommon.AppendSSEEventString(output, event, payload, 3) + } // Initialize the streaming session with a message_start event // This is only sent for the very first response chunk to establish the streaming session if !(*param).(*Params).HasFirstResponse { - output = "event: message_start\n" - // Create the initial message structure with default values according to Claude Code API specification // This follows the Claude Code API specification for streaming message initialization - messageStartTemplate := `{"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-5-sonnet-20241022", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 0, "output_tokens": 0}}}` + messageStartTemplate := []byte(`{"type":"message_start","message":{"id":"msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY","type":"message","role":"assistant","content":[],"model":"claude-3-5-sonnet-20241022","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`) // Override default values with actual response metadata if available from the Gemini CLI response if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.model", modelVersionResult.String()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.model", modelVersionResult.String()) } if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.id", responseIDResult.String()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.id", responseIDResult.String()) } - output = output + fmt.Sprintf("data: %s\n\n\n", messageStartTemplate) + appendEvent("message_start", string(messageStartTemplate)) (*param).(*Params).HasFirstResponse = true } @@ -110,9 +110,8 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque if partResult.Get("thought").Bool() { // Continue existing thinking block if already in thinking state if (*param).(*Params).ResponseType == 2 { - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex)), "delta.thinking", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).HasContent = true } else { // Transition from another state to thinking @@ -123,19 +122,14 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, (*param).(*Params).ResponseIndex) // output = output + "\n\n\n" } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ } // Start a new thinking content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex)) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex)), "delta.thinking", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).ResponseType = 2 // Set state to thinking (*param).(*Params).HasContent = true } @@ -143,9 +137,8 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // Process regular text content (user-visible output) // Continue existing text block if already in content state if (*param).(*Params).ResponseType == 1 { - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex)), "delta.text", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).HasContent = true } else { // Transition from another state to text content @@ -156,19 +149,14 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, (*param).(*Params).ResponseIndex) // output = output + "\n\n\n" } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ } // Start a new text content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex)) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex)), "delta.text", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).ResponseType = 1 // Set state to content (*param).(*Params).HasContent = true } @@ -182,9 +170,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // Handle state transitions when switching to function calls // Close any existing function call block first if (*param).(*Params).ResponseType == 3 { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ (*param).(*Params).ResponseType = 0 } @@ -198,26 +184,21 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // Close any other existing content block if (*param).(*Params).ResponseType != 0 { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ } // Start a new tool use content block // This creates the structure for a function call in Claude Code format - output = output + "event: content_block_start\n" - // Create the tool use block with unique ID and function details - data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex) - data, _ = sjson.Set(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1)))) - data, _ = sjson.Set(data, "content_block.name", fcName) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data := []byte(fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex)) + data, _ = sjson.SetBytes(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&toolUseIDCounter, 1)))) + data, _ = sjson.SetBytes(data, "content_block.name", fcName) + appendEvent("content_block_start", string(data)) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - output = output + "event: content_block_delta\n" - data, _ = sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex), "delta.partial_json", fcArgsResult.Raw) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ = sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex)), "delta.partial_json", fcArgsResult.Raw) + appendEvent("content_block_delta", string(data)) } (*param).(*Params).ResponseType = 3 (*param).(*Params).HasContent = true @@ -232,34 +213,28 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // Only send final events if we have actually output content if (*param).(*Params).HasContent { // Close the final content block - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" - - // Send the final message delta with usage information and stop reason - output = output + "event: message_delta\n" - output = output + `data: ` + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) // Create the message delta template with appropriate stop reason - template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template := []byte(`{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) // Set tool_use stop reason if tools were used in this response if usedTool { - template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template = []byte(`{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) } else if finish := gjson.GetBytes(rawJSON, "response.candidates.0.finishReason"); finish.Exists() && finish.String() == "MAX_TOKENS" { - template = `{"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template = []byte(`{"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) } // Include thinking tokens in output token count if present thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() - template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount) - template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int()) + template, _ = sjson.SetBytes(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount) + template, _ = sjson.SetBytes(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int()) - output = output + template + "\n\n\n" + appendEvent("message_delta", string(template)) } } } - return []string{output} + return [][]byte{output} } // ConvertGeminiCLIResponseToClaudeNonStream converts a non-streaming Gemini CLI response to a non-streaming Claude response. @@ -271,21 +246,21 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque // - param: A pointer to a parameter object for the conversion. // // Returns: -// - string: A Claude-compatible JSON response. -func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Claude-compatible JSON response. +func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { _ = originalRequestRawJSON _ = requestRawJSON root := gjson.ParseBytes(rawJSON) - out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` - out, _ = sjson.Set(out, "id", root.Get("response.responseId").String()) - out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String()) + out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) + out, _ = sjson.SetBytes(out, "id", root.Get("response.responseId").String()) + out, _ = sjson.SetBytes(out, "model", root.Get("response.modelVersion").String()) inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int() outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int() - out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) - out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens) parts := root.Get("response.candidates.0.content.parts") textBuilder := strings.Builder{} @@ -297,9 +272,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig if textBuilder.Len() == 0 { return } - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", textBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", textBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) textBuilder.Reset() } @@ -307,9 +282,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig if thinkingBuilder.Len() == 0 { return } - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) thinkingBuilder.Reset() } @@ -333,15 +308,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig name := functionCall.Get("name").String() toolIDCounter++ - toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` - toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) - toolBlock, _ = sjson.Set(toolBlock, "name", name) + toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolBlock, _ = sjson.SetBytes(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter)) + toolBlock, _ = sjson.SetBytes(toolBlock, "name", name) inputRaw := "{}" if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() { inputRaw = args.Raw } - toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) - out, _ = sjson.SetRaw(out, "content.-1", toolBlock) + toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw)) + out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock) continue } } @@ -365,15 +340,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig } } } - out, _ = sjson.Set(out, "stop_reason", stopReason) + out, _ = sjson.SetBytes(out, "stop_reason", stopReason) if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() { - out, _ = sjson.Delete(out, "usage") + out, _ = sjson.DeleteBytes(out, "usage") } return out } -func ClaudeTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"input_tokens":%d}`, count) +func ClaudeTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.ClaudeInputTokensJSON(count) } diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go index ee6c5b83..9bdce339 100644 --- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go +++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_request.go @@ -34,23 +34,23 @@ import ( // - []byte: The transformed request data in Gemini API format func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []byte { rawJSON := inputRawJSON - template := "" - template = `{"project":"","request":{},"model":""}` - template, _ = sjson.SetRaw(template, "request", string(rawJSON)) - template, _ = sjson.Set(template, "model", gjson.Get(template, "request.model").String()) - template, _ = sjson.Delete(template, "request.model") + template := []byte(`{"project":"","request":{},"model":""}`) + template, _ = sjson.SetRawBytes(template, "request", rawJSON) + template, _ = sjson.SetBytes(template, "model", gjson.GetBytes(template, "request.model").String()) + template, _ = sjson.DeleteBytes(template, "request.model") - template, errFixCLIToolResponse := fixCLIToolResponse(template) + templateStr, errFixCLIToolResponse := fixCLIToolResponse(string(template)) if errFixCLIToolResponse != nil { return []byte{} } + template = []byte(templateStr) - systemInstructionResult := gjson.Get(template, "request.system_instruction") + systemInstructionResult := gjson.GetBytes(template, "request.system_instruction") if systemInstructionResult.Exists() { - template, _ = sjson.SetRaw(template, "request.systemInstruction", systemInstructionResult.Raw) - template, _ = sjson.Delete(template, "request.system_instruction") + template, _ = sjson.SetRawBytes(template, "request.systemInstruction", []byte(systemInstructionResult.Raw)) + template, _ = sjson.DeleteBytes(template, "request.system_instruction") } - rawJSON = []byte(template) + rawJSON = template // Normalize roles in request.contents: default to valid values if missing/invalid contents := gjson.GetBytes(rawJSON, "request.contents") @@ -113,7 +113,7 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by // Filter out contents with empty parts to avoid Gemini API error: // "required oneof field 'data' must have one initialized field" - filteredContents := "[]" + filteredContents := []byte(`[]`) hasFiltered := false gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool { parts := content.Get("parts") @@ -121,11 +121,11 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by hasFiltered = true return true } - filteredContents, _ = sjson.SetRaw(filteredContents, "-1", content.Raw) + filteredContents, _ = sjson.SetRawBytes(filteredContents, "-1", []byte(content.Raw)) return true }) if hasFiltered { - rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContents)) + rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", filteredContents) } return common.AttachDefaultSafetySettings(rawJSON, "request.safetySettings") @@ -142,7 +142,8 @@ type FunctionCallGroup struct { func backfillFunctionResponseName(raw string, fallbackName string) string { name := gjson.Get(raw, "functionResponse.name").String() if strings.TrimSpace(name) == "" && fallbackName != "" { - raw, _ = sjson.Set(raw, "functionResponse.name", fallbackName) + rawBytes, _ := sjson.SetBytes([]byte(raw), "functionResponse.name", fallbackName) + raw = string(rawBytes) } return raw } @@ -171,7 +172,7 @@ func fixCLIToolResponse(input string) (string, error) { } // Initialize data structures for processing and grouping - contentsWrapper := `{"contents":[]}` + contentsWrapper := []byte(`{"contents":[]}`) var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses var collectedResponses []gjson.Result // Standalone responses to be matched @@ -204,18 +205,18 @@ func fixCLIToolResponse(input string) (string, error) { collectedResponses = collectedResponses[group.ResponsesNeeded:] // Create merged function response content - functionResponseContent := `{"parts":[],"role":"function"}` + functionResponseContent := []byte(`{"parts":[],"role":"function"}`) for ri, response := range groupResponses { if !response.IsObject() { log.Warnf("failed to parse function response") continue } raw := backfillFunctionResponseName(response.Raw, group.CallNames[ri]) - functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", raw) + functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts.-1", []byte(raw)) } - if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) + if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 { + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent) } } @@ -238,7 +239,7 @@ func fixCLIToolResponse(input string) (string, error) { log.Warnf("failed to parse model content") return true } - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw)) // Create a new group for tracking responses group := &FunctionCallGroup{ @@ -252,7 +253,7 @@ func fixCLIToolResponse(input string) (string, error) { log.Warnf("failed to parse content") return true } - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw)) } } else { // Non-model content (user, etc.) @@ -260,7 +261,7 @@ func fixCLIToolResponse(input string) (string, error) { log.Warnf("failed to parse content") return true } - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw) + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw)) } return true @@ -272,25 +273,25 @@ func fixCLIToolResponse(input string) (string, error) { groupResponses := collectedResponses[:group.ResponsesNeeded] collectedResponses = collectedResponses[group.ResponsesNeeded:] - functionResponseContent := `{"parts":[],"role":"function"}` + functionResponseContent := []byte(`{"parts":[],"role":"function"}`) for ri, response := range groupResponses { if !response.IsObject() { log.Warnf("failed to parse function response") continue } raw := backfillFunctionResponseName(response.Raw, group.CallNames[ri]) - functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", raw) + functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts.-1", []byte(raw)) } - if gjson.Get(functionResponseContent, "parts.#").Int() > 0 { - contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent) + if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 { + contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent) } } } // Update the original JSON with the new contents - result := input - result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw) + result := []byte(input) + result, _ = sjson.SetRawBytes(result, "request.contents", []byte(gjson.GetBytes(contentsWrapper, "contents").Raw)) - return result, nil + return string(result), nil } diff --git a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go index 0ae931f1..8e23f1d3 100644 --- a/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go +++ b/internal/translator/gemini-cli/gemini/gemini-cli_gemini_response.go @@ -8,8 +8,8 @@ package gemini import ( "bytes" "context" - "fmt" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -29,8 +29,8 @@ import ( // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - []string: The transformed request data in Gemini API format -func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string { +// - [][]byte: The transformed request data in Gemini API format +func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte { if bytes.HasPrefix(rawJSON, []byte("data:")) { rawJSON = bytes.TrimSpace(rawJSON[5:]) } @@ -43,22 +43,22 @@ func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalReq chunk = []byte(responseResult.Raw) } } else { - chunkTemplate := "[]" + chunkTemplate := []byte(`[]`) responseResult := gjson.ParseBytes(chunk) if responseResult.IsArray() { responseResultItems := responseResult.Array() for i := 0; i < len(responseResultItems); i++ { responseResultItem := responseResultItems[i] if responseResultItem.Get("response").Exists() { - chunkTemplate, _ = sjson.SetRaw(chunkTemplate, "-1", responseResultItem.Get("response").Raw) + chunkTemplate, _ = sjson.SetRawBytes(chunkTemplate, "-1", []byte(responseResultItem.Get("response").Raw)) } } } - chunk = []byte(chunkTemplate) + chunk = chunkTemplate } - return []string{string(chunk)} + return [][]byte{chunk} } - return []string{} + return [][]byte{} } // ConvertGeminiCliResponseToGeminiNonStream converts a non-streaming Gemini CLI request to a non-streaming Gemini response. @@ -72,15 +72,15 @@ func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalReq // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: A Gemini-compatible JSON response containing the response data -func ConvertGeminiCliResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Gemini-compatible JSON response containing the response data +func ConvertGeminiCliResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { - return responseResult.Raw + return []byte(responseResult.Raw) } - return string(rawJSON) + return rawJSON } -func GeminiTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go index b0a6bddd..0fed7623 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go @@ -299,43 +299,43 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo if t.Get("type").String() == "function" { fn := t.Get("function") if fn.Exists() && fn.IsObject() { - fnRaw := fn.Raw + fnRaw := []byte(fn.Raw) if fn.Get("parameters").Exists() { - renamed, errRename := util.RenameKey(fnRaw, "parameters", "parametersJsonSchema") + renamed, errRename := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema") if errRename != nil { log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename) var errSet error - fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object") + fnRaw, errSet = sjson.SetBytes(fnRaw, "parametersJsonSchema.type", "object") if errSet != nil { log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) continue } - fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) + fnRaw, errSet = sjson.SetRawBytes(fnRaw, "parametersJsonSchema.properties", []byte(`{}`)) if errSet != nil { log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) continue } } else { - fnRaw = renamed + fnRaw = []byte(renamed) } } else { var errSet error - fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object") + fnRaw, errSet = sjson.SetBytes(fnRaw, "parametersJsonSchema.type", "object") if errSet != nil { log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) continue } - fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) + fnRaw, errSet = sjson.SetRawBytes(fnRaw, "parametersJsonSchema.properties", []byte(`{}`)) if errSet != nil { log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) continue } } - fnRaw, _ = sjson.Delete(fnRaw, "strict") + fnRaw, _ = sjson.DeleteBytes(fnRaw, "strict") if !hasFunction { functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]")) } - tmp, errSet := sjson.SetRawBytes(functionToolNode, "functionDeclarations.-1", []byte(fnRaw)) + tmp, errSet := sjson.SetRawBytes(functionToolNode, "functionDeclarations.-1", fnRaw) if errSet != nil { log.Warnf("Failed to append tool declaration for '%s': %v", fn.Get("name").String(), errSet) continue diff --git a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go index b26d431f..faec3b35 100644 --- a/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go +++ b/internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_response.go @@ -41,8 +41,8 @@ var functionCallIDCounter uint64 // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing an OpenAI-compatible JSON response -func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of OpenAI-compatible JSON responses +func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &convertCliResponseToOpenAIChatParams{ UnixTimestamp: 0, @@ -51,15 +51,15 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ } if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } // Initialize the OpenAI SSE template. - template := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}` + template := []byte(`{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`) // Extract and set the model version. if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() { - template, _ = sjson.Set(template, "model", modelVersionResult.String()) + template, _ = sjson.SetBytes(template, "model", modelVersionResult.String()) } // Extract and set the creation timestamp. @@ -68,14 +68,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ if err == nil { (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp = t.Unix() } - template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) + template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) } else { - template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) + template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp) } // Extract and set the response ID. if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() { - template, _ = sjson.Set(template, "id", responseIDResult.String()) + template, _ = sjson.SetBytes(template, "id", responseIDResult.String()) } finishReason := "" @@ -93,21 +93,21 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() { cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int() if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) } if totalTokenCountResult := usageResult.Get("totalTokenCount"); totalTokenCountResult.Exists() { - template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int()) + template, _ = sjson.SetBytes(template, "usage.total_tokens", totalTokenCountResult.Int()) } promptTokenCount := usageResult.Get("promptTokenCount").Int() thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() - template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCount) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens", promptTokenCount) if thoughtsTokenCount > 0 { - template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) + template, _ = sjson.SetBytes(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) } // Include cached token count if present (indicates prompt caching is working) if cachedTokenCount > 0 { var err error - template, err = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) + template, err = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) if err != nil { log.Warnf("gemini-cli openai response: failed to set cached_tokens: %v", err) } @@ -145,33 +145,33 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ // Handle text content, distinguishing between regular content and reasoning/thoughts. if partResult.Get("thought").Bool() { - template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", textContent) + template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", textContent) } else { - template, _ = sjson.Set(template, "choices.0.delta.content", textContent) + template, _ = sjson.SetBytes(template, "choices.0.delta.content", textContent) } - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") } else if functionCallResult.Exists() { // Handle function call content. hasFunctionCall = true - toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls") + toolCallsResult := gjson.GetBytes(template, "choices.0.delta.tool_calls") functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++ if toolCallsResult.Exists() && toolCallsResult.IsArray() { functionCallIndex = len(toolCallsResult.Array()) } else { - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) } - functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` + functionCallTemplate := []byte(`{"id":"","index":0,"type":"function","function":{"name":"","arguments":""}}`) fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", fcArgsResult.Raw) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.arguments", fcArgsResult.Raw) } - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallTemplate) } else if inlineDataResult.Exists() { data := inlineDataResult.Get("data").String() if data == "" { @@ -185,32 +185,32 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ mimeType = "image/png" } imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) - imagesResult := gjson.Get(template, "choices.0.delta.images") + imagesResult := gjson.GetBytes(template, "choices.0.delta.images") if !imagesResult.Exists() || !imagesResult.IsArray() { - template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images", []byte(`[]`)) } - imageIndex := len(gjson.Get(template, "choices.0.delta.images").Array()) - imagePayload := `{"type":"image_url","image_url":{"url":""}}` - imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex) - imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload) + imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array()) + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload) } } } if hasFunctionCall { - template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls") - template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls") + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", "tool_calls") + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", "tool_calls") } else if finishReason != "" && (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex == 0 { // Only pass through specific finish reasons if finishReason == "max_tokens" || finishReason == "stop" { - template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason) - template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", finishReason) } } - return []string{template} + return [][]byte{template} } // ConvertCliResponseToOpenAINonStream converts a non-streaming Gemini CLI response to a non-streaming OpenAI response. @@ -225,11 +225,11 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ // - param: A pointer to a parameter object for the conversion // // Returns: -// - string: An OpenAI-compatible JSON response containing all message content and metadata -func ConvertCliResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +// - []byte: An OpenAI-compatible JSON response containing all message content and metadata +func ConvertCliResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { return ConvertGeminiResponseToOpenAINonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, []byte(responseResult.Raw), param) } - return "" + return []byte{} } diff --git a/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go index 51865884..9bb3ced9 100644 --- a/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go +++ b/internal/translator/gemini-cli/openai/responses/gemini-cli_openai-responses_response.go @@ -7,7 +7,7 @@ import ( "github.com/tidwall/gjson" ) -func ConvertGeminiCLIResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func ConvertGeminiCLIResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { rawJSON = []byte(responseResult.Raw) @@ -15,7 +15,7 @@ func ConvertGeminiCLIResponseToOpenAIResponses(ctx context.Context, modelName st return ConvertGeminiResponseToOpenAIResponses(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) } -func ConvertGeminiCLIResponseToOpenAIResponsesNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +func ConvertGeminiCLIResponseToOpenAIResponsesNonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { responseResult := gjson.GetBytes(rawJSON, "response") if responseResult.Exists() { rawJSON = []byte(responseResult.Raw) diff --git a/internal/translator/gemini/claude/gemini_claude_request.go b/internal/translator/gemini/claude/gemini_claude_request.go index 137008b0..bd3eaffe 100644 --- a/internal/translator/gemini/claude/gemini_claude_request.go +++ b/internal/translator/gemini/claude/gemini_claude_request.go @@ -33,30 +33,30 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1) // Build output Gemini CLI request JSON - out := `{"contents":[]}` - out, _ = sjson.Set(out, "model", modelName) + out := []byte(`{"contents":[]}`) + out, _ = sjson.SetBytes(out, "model", modelName) // system instruction if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() { - systemInstruction := `{"role":"user","parts":[]}` + systemInstruction := []byte(`{"role":"user","parts":[]}`) hasSystemParts := false systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool { if systemPromptResult.Get("type").String() == "text" { textResult := systemPromptResult.Get("text") if textResult.Type == gjson.String { - part := `{"text":""}` - part, _ = sjson.Set(part, "text", textResult.String()) - systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", textResult.String()) + systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part) hasSystemParts = true } } return true }) if hasSystemParts { - out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction) + out, _ = sjson.SetRawBytes(out, "system_instruction", systemInstruction) } } else if systemResult.Type == gjson.String { - out, _ = sjson.Set(out, "system_instruction.parts.-1.text", systemResult.String()) + out, _ = sjson.SetBytes(out, "system_instruction.parts.-1.text", systemResult.String()) } // contents @@ -71,17 +71,17 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) role = "model" } - contentJSON := `{"role":"","parts":[]}` - contentJSON, _ = sjson.Set(contentJSON, "role", role) + contentJSON := []byte(`{"role":"","parts":[]}`) + contentJSON, _ = sjson.SetBytes(contentJSON, "role", role) contentsResult := messageResult.Get("content") if contentsResult.IsArray() { contentsResult.ForEach(func(_, contentResult gjson.Result) bool { switch contentResult.Get("type").String() { case "text": - part := `{"text":""}` - part, _ = sjson.Set(part, "text", contentResult.Get("text").String()) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", contentResult.Get("text").String()) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) case "tool_use": functionName := contentResult.Get("name").String() @@ -93,11 +93,11 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) functionArgs := contentResult.Get("input").String() argsResult := gjson.Parse(functionArgs) if argsResult.IsObject() && gjson.Valid(functionArgs) { - part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}` - part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature) - part, _ = sjson.Set(part, "functionCall.name", functionName) - part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`) + part, _ = sjson.SetBytes(part, "thoughtSignature", geminiClaudeThoughtSignature) + part, _ = sjson.SetBytes(part, "functionCall.name", functionName) + part, _ = sjson.SetRawBytes(part, "functionCall.args", []byte(functionArgs)) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) } case "tool_result": @@ -110,10 +110,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) funcName = toolCallID } responseData := contentResult.Get("content").Raw - part := `{"functionResponse":{"name":"","response":{"result":""}}}` - part, _ = sjson.Set(part, "functionResponse.name", funcName) - part, _ = sjson.Set(part, "functionResponse.response.result", responseData) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`) + part, _ = sjson.SetBytes(part, "functionResponse.name", funcName) + part, _ = sjson.SetBytes(part, "functionResponse.response.result", responseData) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) case "image": source := contentResult.Get("source") @@ -125,19 +125,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) if mimeType == "" || data == "" { return true } - part := `{"inline_data":{"mime_type":"","data":""}}` - part, _ = sjson.Set(part, "inline_data.mime_type", mimeType) - part, _ = sjson.Set(part, "inline_data.data", data) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) + part := []byte(`{"inline_data":{"mime_type":"","data":""}}`) + part, _ = sjson.SetBytes(part, "inline_data.mime_type", mimeType) + part, _ = sjson.SetBytes(part, "inline_data.data", data) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) } return true }) - out, _ = sjson.SetRaw(out, "contents.-1", contentJSON) + out, _ = sjson.SetRawBytes(out, "contents.-1", contentJSON) } else if contentsResult.Type == gjson.String { - part := `{"text":""}` - part, _ = sjson.Set(part, "text", contentsResult.String()) - contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part) - out, _ = sjson.SetRaw(out, "contents.-1", contentJSON) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", contentsResult.String()) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part) + out, _ = sjson.SetRawBytes(out, "contents.-1", contentJSON) } return true }) @@ -150,25 +150,33 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) inputSchemaResult := toolResult.Get("input_schema") if inputSchemaResult.Exists() && inputSchemaResult.IsObject() { inputSchema := inputSchemaResult.Raw - tool, _ := sjson.Delete(toolResult.Raw, "input_schema") - tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema) - tool, _ = sjson.Delete(tool, "strict") - tool, _ = sjson.Delete(tool, "input_examples") - tool, _ = sjson.Delete(tool, "type") - tool, _ = sjson.Delete(tool, "cache_control") - tool, _ = sjson.Delete(tool, "defer_loading") - if gjson.Valid(tool) && gjson.Parse(tool).IsObject() { + tool := []byte(toolResult.Raw) + var err error + tool, err = sjson.DeleteBytes(tool, "input_schema") + if err != nil { + return true + } + tool, err = sjson.SetRawBytes(tool, "parametersJsonSchema", []byte(inputSchema)) + if err != nil { + return true + } + tool, _ = sjson.DeleteBytes(tool, "strict") + tool, _ = sjson.DeleteBytes(tool, "input_examples") + tool, _ = sjson.DeleteBytes(tool, "type") + tool, _ = sjson.DeleteBytes(tool, "cache_control") + tool, _ = sjson.DeleteBytes(tool, "defer_loading") + if gjson.ValidBytes(tool) && gjson.ParseBytes(tool).IsObject() { if !hasTools { - out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`) + out, _ = sjson.SetRawBytes(out, "tools", []byte(`[{"functionDeclarations":[]}]`)) hasTools = true } - out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool) + out, _ = sjson.SetRawBytes(out, "tools.0.functionDeclarations.-1", tool) } } return true }) if !hasTools { - out, _ = sjson.Delete(out, "tools") + out, _ = sjson.DeleteBytes(out, "tools") } } @@ -186,15 +194,15 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) switch toolChoiceType { case "auto": - out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "AUTO") + out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO") case "none": - out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "NONE") + out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "NONE") case "any": - out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "ANY") + out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "ANY") case "tool": - out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "ANY") + out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "ANY") if toolChoiceName != "" { - out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) + out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.allowedFunctionNames", []string{toolChoiceName}) } } } @@ -206,8 +214,8 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) case "enabled": if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number { budget := int(b.Int()) - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget) - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true) + out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingBudget", budget) + out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.includeThoughts", true) } case "adaptive", "auto": // For adaptive thinking: @@ -219,32 +227,32 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool) effort = strings.ToLower(strings.TrimSpace(v.String())) } if effort != "" { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", effort) + out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingLevel", effort) } else { maxBudget := 0 if mi := registry.LookupModelInfo(modelName, "gemini"); mi != nil && mi.Thinking != nil { maxBudget = mi.Thinking.Max } if maxBudget > 0 { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", maxBudget) + out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingBudget", maxBudget) } else { - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", "high") + out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingLevel", "high") } } - out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true) + out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.includeThoughts", true) } } if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "generationConfig.temperature", v.Num) + out, _ = sjson.SetBytes(out, "generationConfig.temperature", v.Num) } if v := gjson.GetBytes(rawJSON, "top_p"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "generationConfig.topP", v.Num) + out, _ = sjson.SetBytes(out, "generationConfig.topP", v.Num) } if v := gjson.GetBytes(rawJSON, "top_k"); v.Exists() && v.Type == gjson.Number { - out, _ = sjson.Set(out, "generationConfig.topK", v.Num) + out, _ = sjson.SetBytes(out, "generationConfig.topK", v.Num) } - result := []byte(out) + result := out result = common.AttachDefaultSafetySettings(result, "safetySettings") return result diff --git a/internal/translator/gemini/claude/gemini_claude_response.go b/internal/translator/gemini/claude/gemini_claude_response.go index eeb4af11..9b21b009 100644 --- a/internal/translator/gemini/claude/gemini_claude_response.go +++ b/internal/translator/gemini/claude/gemini_claude_response.go @@ -13,6 +13,7 @@ import ( "strings" "sync/atomic" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -47,8 +48,8 @@ var toolUseIDCounter uint64 // - param: A pointer to a parameter object for the conversion. // // Returns: -// - []string: A slice of strings, each containing a Claude-compatible JSON response. -func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of bytes, each containing a Claude-compatible SSE payload. +func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &Params{ IsGlAPIKey: false, @@ -63,32 +64,31 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR if bytes.Equal(rawJSON, []byte("[DONE]")) { // Only send message_stop if we have actually output content if (*param).(*Params).HasContent { - return []string{ - "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n", - } + return [][]byte{translatorcommon.AppendSSEEventString(nil, "message_stop", `{"type":"message_stop"}`, 3)} } - return []string{} + return [][]byte{} } - output := "" + output := make([]byte, 0, 1024) + appendEvent := func(event, payload string) { + output = translatorcommon.AppendSSEEventString(output, event, payload, 3) + } // Initialize the streaming session with a message_start event // This is only sent for the very first response chunk if !(*param).(*Params).HasFirstResponse { - output = "event: message_start\n" - // Create the initial message structure with default values // This follows the Claude API specification for streaming message initialization - messageStartTemplate := `{"type": "message_start", "message": {"id": "msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY", "type": "message", "role": "assistant", "content": [], "model": "claude-3-5-sonnet-20241022", "stop_reason": null, "stop_sequence": null, "usage": {"input_tokens": 0, "output_tokens": 0}}}` + messageStartTemplate := []byte(`{"type":"message_start","message":{"id":"msg_1nZdL29xx5MUA1yADyHTEsnR8uuvGzszyY","type":"message","role":"assistant","content":[],"model":"claude-3-5-sonnet-20241022","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`) // Override default values with actual response metadata if available if modelVersionResult := gjson.GetBytes(rawJSON, "modelVersion"); modelVersionResult.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.model", modelVersionResult.String()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.model", modelVersionResult.String()) } if responseIDResult := gjson.GetBytes(rawJSON, "responseId"); responseIDResult.Exists() { - messageStartTemplate, _ = sjson.Set(messageStartTemplate, "message.id", responseIDResult.String()) + messageStartTemplate, _ = sjson.SetBytes(messageStartTemplate, "message.id", responseIDResult.String()) } - output = output + fmt.Sprintf("data: %s\n\n\n", messageStartTemplate) + appendEvent("message_start", string(messageStartTemplate)) (*param).(*Params).HasFirstResponse = true } @@ -111,9 +111,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR if partResult.Get("thought").Bool() { // Continue existing thinking block if (*param).(*Params).ResponseType == 2 { - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex)), "delta.thinking", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).HasContent = true } else { // Transition from another state to thinking @@ -124,19 +123,14 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, (*param).(*Params).ResponseIndex) // output = output + "\n\n\n" } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ } // Start a new thinking content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex), "delta.thinking", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex)) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex)), "delta.thinking", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).ResponseType = 2 // Set state to thinking (*param).(*Params).HasContent = true } @@ -144,9 +138,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // Process regular text content (user-visible output) // Continue existing text block if (*param).(*Params).ResponseType == 1 { - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex)), "delta.text", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).HasContent = true } else { // Transition from another state to text content @@ -157,19 +150,14 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // output = output + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, (*param).(*Params).ResponseIndex) // output = output + "\n\n\n" } - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ } // Start a new text content block - output = output + "event: content_block_start\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex), "delta.text", partTextResult.String()) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex)) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex)), "delta.text", partTextResult.String()) + appendEvent("content_block_delta", string(data)) (*param).(*Params).ResponseType = 1 // Set state to content (*param).(*Params).HasContent = true } @@ -185,9 +173,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // If we are already in tool use mode and name is empty, treat as continuation (delta). if (*param).(*Params).ResponseType == 3 && upstreamToolName == "" { if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - output = output + "event: content_block_delta\n" - data, _ := sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex), "delta.partial_json", fcArgsResult.Raw) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex)), "delta.partial_json", fcArgsResult.Raw) + appendEvent("content_block_delta", string(data)) } // Continue to next part without closing/opening logic continue @@ -196,9 +183,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // Handle state transitions when switching to function calls // Close any existing function call block first if (*param).(*Params).ResponseType == 3 { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ (*param).(*Params).ResponseType = 0 } @@ -212,26 +197,21 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // Close any other existing content block if (*param).(*Params).ResponseType != 0 { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) (*param).(*Params).ResponseIndex++ } // Start a new tool use content block // This creates the structure for a function call in Claude format - output = output + "event: content_block_start\n" - // Create the tool use block with unique ID and function details - data := fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex) - data, _ = sjson.Set(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, atomic.AddUint64(&toolUseIDCounter, 1)))) - data, _ = sjson.Set(data, "content_block.name", clientToolName) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data := []byte(fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`, (*param).(*Params).ResponseIndex)) + data, _ = sjson.SetBytes(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, atomic.AddUint64(&toolUseIDCounter, 1)))) + data, _ = sjson.SetBytes(data, "content_block.name", clientToolName) + appendEvent("content_block_start", string(data)) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - output = output + "event: content_block_delta\n" - data, _ = sjson.Set(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex), "delta.partial_json", fcArgsResult.Raw) - output = output + fmt.Sprintf("data: %s\n\n\n", data) + data, _ = sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"input_json_delta","partial_json":""}}`, (*param).(*Params).ResponseIndex)), "delta.partial_json", fcArgsResult.Raw) + appendEvent("content_block_delta", string(data)) } (*param).(*Params).ResponseType = 3 (*param).(*Params).HasContent = true @@ -244,30 +224,25 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { // Only send final events if we have actually output content if (*param).(*Params).HasContent { - output = output + "event: content_block_stop\n" - output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex) - output = output + "\n\n\n" + appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)) - output = output + "event: message_delta\n" - output = output + `data: ` - - template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template := []byte(`{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) if (*param).(*Params).SawToolCall { - template = `{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template = []byte(`{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) } else if finish := gjson.GetBytes(rawJSON, "candidates.0.finishReason"); finish.Exists() && finish.String() == "MAX_TOKENS" { - template = `{"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` + template = []byte(`{"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) } thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() - template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount) - template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int()) + template, _ = sjson.SetBytes(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount) + template, _ = sjson.SetBytes(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int()) - output = output + template + "\n\n\n" + appendEvent("message_delta", string(template)) } } } - return []string{output} + return [][]byte{output} } // ConvertGeminiResponseToClaudeNonStream converts a non-streaming Gemini response to a non-streaming Claude response. @@ -279,21 +254,21 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR // - param: A pointer to a parameter object for the conversion. // // Returns: -// - string: A Claude-compatible JSON response. -func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Claude-compatible JSON response. +func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { _ = requestRawJSON root := gjson.ParseBytes(rawJSON) toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON) - out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` - out, _ = sjson.Set(out, "id", root.Get("responseId").String()) - out, _ = sjson.Set(out, "model", root.Get("modelVersion").String()) + out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) + out, _ = sjson.SetBytes(out, "id", root.Get("responseId").String()) + out, _ = sjson.SetBytes(out, "model", root.Get("modelVersion").String()) inputTokens := root.Get("usageMetadata.promptTokenCount").Int() outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int() - out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) - out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens) parts := root.Get("candidates.0.content.parts") textBuilder := strings.Builder{} @@ -305,9 +280,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina if textBuilder.Len() == 0 { return } - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", textBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", textBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) textBuilder.Reset() } @@ -315,9 +290,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina if thinkingBuilder.Len() == 0 { return } - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) thinkingBuilder.Reset() } @@ -342,15 +317,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina upstreamToolName := functionCall.Get("name").String() clientToolName := util.MapToolName(toolNameMap, upstreamToolName) toolIDCounter++ - toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}` - toolBlock, _ = sjson.Set(toolBlock, "id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, toolIDCounter))) - toolBlock, _ = sjson.Set(toolBlock, "name", clientToolName) + toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolBlock, _ = sjson.SetBytes(toolBlock, "id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, toolIDCounter))) + toolBlock, _ = sjson.SetBytes(toolBlock, "name", clientToolName) inputRaw := "{}" if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() { inputRaw = args.Raw } - toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw) - out, _ = sjson.SetRaw(out, "content.-1", toolBlock) + toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw)) + out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock) continue } } @@ -374,15 +349,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina } } } - out, _ = sjson.Set(out, "stop_reason", stopReason) + out, _ = sjson.SetBytes(out, "stop_reason", stopReason) if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() { - out, _ = sjson.Delete(out, "usage") + out, _ = sjson.DeleteBytes(out, "usage") } return out } -func ClaudeTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"input_tokens":%d}`, count) +func ClaudeTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.ClaudeInputTokensJSON(count) } diff --git a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go index 39b8dfb6..d15ea21a 100644 --- a/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go +++ b/internal/translator/gemini/gemini-cli/gemini_gemini-cli_response.go @@ -7,8 +7,8 @@ package geminiCLI import ( "bytes" "context" - "fmt" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/sjson" ) @@ -26,19 +26,18 @@ var dataTag = []byte("data:") // - param: A pointer to a parameter object for the conversion (unused). // // Returns: -// - []string: A slice of strings, each containing a Gemini CLI-compatible JSON response. -func ConvertGeminiResponseToGeminiCLI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string { +// - [][]byte: A slice of Gemini CLI-compatible JSON responses. +func ConvertGeminiResponseToGeminiCLI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte { if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } - json := `{"response": {}}` - rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON) - return []string{string(rawJSON)} + rawJSON, _ = sjson.SetRawBytes([]byte(`{"response":{}}`), "response", rawJSON) + return [][]byte{rawJSON} } // ConvertGeminiResponseToGeminiCLINonStream converts a non-streaming Gemini response to a non-streaming Gemini CLI response. @@ -50,13 +49,12 @@ func ConvertGeminiResponseToGeminiCLI(_ context.Context, _ string, originalReque // - param: A pointer to a parameter object for the conversion (unused). // // Returns: -// - string: A Gemini CLI-compatible JSON response. -func ConvertGeminiResponseToGeminiCLINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { - json := `{"response": {}}` - rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON) - return string(rawJSON) +// - []byte: A Gemini CLI-compatible JSON response. +func ConvertGeminiResponseToGeminiCLINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { + rawJSON, _ = sjson.SetRawBytes([]byte(`{"response":{}}`), "response", rawJSON) + return rawJSON } -func GeminiCLITokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiCLITokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } diff --git a/internal/translator/gemini/gemini/gemini_gemini_response.go b/internal/translator/gemini/gemini/gemini_gemini_response.go index 05fb6ab9..242dd980 100644 --- a/internal/translator/gemini/gemini/gemini_gemini_response.go +++ b/internal/translator/gemini/gemini/gemini_gemini_response.go @@ -3,27 +3,28 @@ package gemini import ( "bytes" "context" - "fmt" + + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" ) // PassthroughGeminiResponseStream forwards Gemini responses unchanged. -func PassthroughGeminiResponseStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string { +func PassthroughGeminiResponseStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte { if bytes.HasPrefix(rawJSON, []byte("data:")) { rawJSON = bytes.TrimSpace(rawJSON[5:]) } if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } - return []string{string(rawJSON)} + return [][]byte{rawJSON} } // PassthroughGeminiResponseNonStream forwards Gemini responses unchanged. -func PassthroughGeminiResponseNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { - return string(rawJSON) +func PassthroughGeminiResponseNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { + return rawJSON } -func GeminiTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go index c8948ac5..39b08d9d 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_request.go @@ -311,31 +311,35 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool) if errRename != nil { log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename) var errSet error - fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object") + fnRawBytes := []byte(fnRaw) + fnRawBytes, errSet = sjson.SetBytes(fnRawBytes, "parametersJsonSchema.type", "object") if errSet != nil { log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) continue } - fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) + fnRawBytes, errSet = sjson.SetRawBytes(fnRawBytes, "parametersJsonSchema.properties", []byte(`{}`)) if errSet != nil { log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) continue } + fnRaw = string(fnRawBytes) } else { fnRaw = renamed } } else { var errSet error - fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object") + fnRawBytes := []byte(fnRaw) + fnRawBytes, errSet = sjson.SetBytes(fnRawBytes, "parametersJsonSchema.type", "object") if errSet != nil { log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet) continue } - fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`) + fnRawBytes, errSet = sjson.SetRawBytes(fnRawBytes, "parametersJsonSchema.properties", []byte(`{}`)) if errSet != nil { log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet) continue } + fnRaw = string(fnRawBytes) } fnRaw, _ = sjson.Delete(fnRaw, "strict") if !hasFunction { diff --git a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go index aeec5e9e..29be1d3a 100644 --- a/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go +++ b/internal/translator/gemini/openai/chat-completions/gemini_openai_response.go @@ -41,8 +41,8 @@ var functionCallIDCounter uint64 // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing an OpenAI-compatible JSON response -func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of OpenAI-compatible JSON responses +func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { // Initialize parameters if nil. if *param == nil { *param = &convertGeminiResponseToOpenAIChatParams{ @@ -62,16 +62,16 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR } if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } // Initialize the OpenAI SSE base template. // We use a base template and clone it for each candidate to support multiple candidates. - baseTemplate := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}` + baseTemplate := []byte(`{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`) // Extract and set the model version. if modelVersionResult := gjson.GetBytes(rawJSON, "modelVersion"); modelVersionResult.Exists() { - baseTemplate, _ = sjson.Set(baseTemplate, "model", modelVersionResult.String()) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "model", modelVersionResult.String()) } // Extract and set the creation timestamp. @@ -80,14 +80,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR if err == nil { p.UnixTimestamp = t.Unix() } - baseTemplate, _ = sjson.Set(baseTemplate, "created", p.UnixTimestamp) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "created", p.UnixTimestamp) } else { - baseTemplate, _ = sjson.Set(baseTemplate, "created", p.UnixTimestamp) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "created", p.UnixTimestamp) } // Extract and set the response ID. if responseIDResult := gjson.GetBytes(rawJSON, "responseId"); responseIDResult.Exists() { - baseTemplate, _ = sjson.Set(baseTemplate, "id", responseIDResult.String()) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "id", responseIDResult.String()) } // Extract and set usage metadata (token counts). @@ -95,39 +95,39 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR if usageResult := gjson.GetBytes(rawJSON, "usageMetadata"); usageResult.Exists() { cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int() if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { - baseTemplate, _ = sjson.Set(baseTemplate, "usage.completion_tokens", candidatesTokenCountResult.Int()) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "usage.completion_tokens", candidatesTokenCountResult.Int()) } if totalTokenCountResult := usageResult.Get("totalTokenCount"); totalTokenCountResult.Exists() { - baseTemplate, _ = sjson.Set(baseTemplate, "usage.total_tokens", totalTokenCountResult.Int()) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "usage.total_tokens", totalTokenCountResult.Int()) } promptTokenCount := usageResult.Get("promptTokenCount").Int() thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() - baseTemplate, _ = sjson.Set(baseTemplate, "usage.prompt_tokens", promptTokenCount) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "usage.prompt_tokens", promptTokenCount) if thoughtsTokenCount > 0 { - baseTemplate, _ = sjson.Set(baseTemplate, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) + baseTemplate, _ = sjson.SetBytes(baseTemplate, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) } // Include cached token count if present (indicates prompt caching is working) if cachedTokenCount > 0 { var err error - baseTemplate, err = sjson.Set(baseTemplate, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) + baseTemplate, err = sjson.SetBytes(baseTemplate, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) if err != nil { log.Warnf("gemini openai response: failed to set cached_tokens in streaming: %v", err) } } } - var responseStrings []string + var responseStrings [][]byte candidates := gjson.GetBytes(rawJSON, "candidates") // Iterate over all candidates to support candidate_count > 1. if candidates.IsArray() { candidates.ForEach(func(_, candidate gjson.Result) bool { // Clone the template for the current candidate. - template := baseTemplate + template := append([]byte(nil), baseTemplate...) // Set the specific index for this candidate. candidateIndex := int(candidate.Get("index").Int()) - template, _ = sjson.Set(template, "choices.0.index", candidateIndex) + template, _ = sjson.SetBytes(template, "choices.0.index", candidateIndex) finishReason := "" if stopReasonResult := gjson.GetBytes(rawJSON, "stop_reason"); stopReasonResult.Exists() { @@ -170,15 +170,15 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR text := partTextResult.String() // Handle text content, distinguishing between regular content and reasoning/thoughts. if partResult.Get("thought").Bool() { - template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", text) + template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", text) } else { - template, _ = sjson.Set(template, "choices.0.delta.content", text) + template, _ = sjson.SetBytes(template, "choices.0.delta.content", text) } - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") } else if functionCallResult.Exists() { // Handle function call content. hasFunctionCall = true - toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls") + toolCallsResult := gjson.GetBytes(template, "choices.0.delta.tool_calls") // Retrieve the function index for this specific candidate. functionCallIndex := p.FunctionIndex[candidateIndex] @@ -187,19 +187,19 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR if toolCallsResult.Exists() && toolCallsResult.IsArray() { functionCallIndex = len(toolCallsResult.Array()) } else { - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`)) } - functionCallTemplate := `{"id": "","index": 0,"type": "function","function": {"name": "","arguments": ""}}` + functionCallTemplate := []byte(`{"id":"","index":0,"type":"function","function":{"name":"","arguments":""}}`) fcName := functionCallResult.Get("name").String() - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex) - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.arguments", fcArgsResult.Raw) + functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.arguments", fcArgsResult.Raw) } - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallTemplate) } else if inlineDataResult.Exists() { data := inlineDataResult.Get("data").String() if data == "" { @@ -213,28 +213,28 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR mimeType = "image/png" } imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) - imagesResult := gjson.Get(template, "choices.0.delta.images") + imagesResult := gjson.GetBytes(template, "choices.0.delta.images") if !imagesResult.Exists() || !imagesResult.IsArray() { - template, _ = sjson.SetRaw(template, "choices.0.delta.images", `[]`) + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images", []byte(`[]`)) } - imageIndex := len(gjson.Get(template, "choices.0.delta.images").Array()) - imagePayload := `{"type":"image_url","image_url":{"url":""}}` - imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex) - imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) - template, _ = sjson.Set(template, "choices.0.delta.role", "assistant") - template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload) + imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array()) + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant") + template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload) } } } if hasFunctionCall { - template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls") - template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls") + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", "tool_calls") + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", "tool_calls") } else if finishReason != "" { // Only pass through specific finish reasons if finishReason == "max_tokens" || finishReason == "stop" { - template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason) - template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason) + template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", finishReason) } } @@ -244,7 +244,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR } else { // If there are no candidates (e.g., a pure usageMetadata chunk), return the usage chunk if present. if gjson.GetBytes(rawJSON, "usageMetadata").Exists() && len(responseStrings) == 0 { - responseStrings = append(responseStrings, baseTemplate) + responseStrings = append(responseStrings, append([]byte(nil), baseTemplate...)) } } @@ -263,14 +263,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR // - param: A pointer to a parameter object for the conversion (unused in current implementation) // // Returns: -// - string: An OpenAI-compatible JSON response containing all message content and metadata -func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: An OpenAI-compatible JSON response containing all message content and metadata +func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { var unixTimestamp int64 // Initialize template with an empty choices array to support multiple candidates. - template := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[]}` + template := []byte(`{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[]}`) if modelVersionResult := gjson.GetBytes(rawJSON, "modelVersion"); modelVersionResult.Exists() { - template, _ = sjson.Set(template, "model", modelVersionResult.String()) + template, _ = sjson.SetBytes(template, "model", modelVersionResult.String()) } if createTimeResult := gjson.GetBytes(rawJSON, "createTime"); createTimeResult.Exists() { @@ -278,33 +278,33 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina if err == nil { unixTimestamp = t.Unix() } - template, _ = sjson.Set(template, "created", unixTimestamp) + template, _ = sjson.SetBytes(template, "created", unixTimestamp) } else { - template, _ = sjson.Set(template, "created", unixTimestamp) + template, _ = sjson.SetBytes(template, "created", unixTimestamp) } if responseIDResult := gjson.GetBytes(rawJSON, "responseId"); responseIDResult.Exists() { - template, _ = sjson.Set(template, "id", responseIDResult.String()) + template, _ = sjson.SetBytes(template, "id", responseIDResult.String()) } if usageResult := gjson.GetBytes(rawJSON, "usageMetadata"); usageResult.Exists() { if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() { - template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) + template, _ = sjson.SetBytes(template, "usage.completion_tokens", candidatesTokenCountResult.Int()) } if totalTokenCountResult := usageResult.Get("totalTokenCount"); totalTokenCountResult.Exists() { - template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int()) + template, _ = sjson.SetBytes(template, "usage.total_tokens", totalTokenCountResult.Int()) } promptTokenCount := usageResult.Get("promptTokenCount").Int() thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int() cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int() - template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCount) + template, _ = sjson.SetBytes(template, "usage.prompt_tokens", promptTokenCount) if thoughtsTokenCount > 0 { - template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) + template, _ = sjson.SetBytes(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCount) } // Include cached token count if present (indicates prompt caching is working) if cachedTokenCount > 0 { var err error - template, err = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) + template, err = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cachedTokenCount) if err != nil { log.Warnf("gemini openai response: failed to set cached_tokens in non-streaming: %v", err) } @@ -316,15 +316,15 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina if candidates.IsArray() { candidates.ForEach(func(_, candidate gjson.Result) bool { // Construct a single Choice object. - choiceTemplate := `{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}` + choiceTemplate := []byte(`{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}`) // Set the index for this choice. - choiceTemplate, _ = sjson.Set(choiceTemplate, "index", candidate.Get("index").Int()) + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "index", candidate.Get("index").Int()) // Set finish reason. if finishReasonResult := candidate.Get("finishReason"); finishReasonResult.Exists() { - choiceTemplate, _ = sjson.Set(choiceTemplate, "finish_reason", strings.ToLower(finishReasonResult.String())) - choiceTemplate, _ = sjson.Set(choiceTemplate, "native_finish_reason", strings.ToLower(finishReasonResult.String())) + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "finish_reason", strings.ToLower(finishReasonResult.String())) + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "native_finish_reason", strings.ToLower(finishReasonResult.String())) } partsResult := candidate.Get("content.parts") @@ -343,29 +343,29 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina if partTextResult.Exists() { // Append text content, distinguishing between regular content and reasoning. if partResult.Get("thought").Bool() { - oldVal := gjson.Get(choiceTemplate, "message.reasoning_content").String() - choiceTemplate, _ = sjson.Set(choiceTemplate, "message.reasoning_content", oldVal+partTextResult.String()) + oldVal := gjson.GetBytes(choiceTemplate, "message.reasoning_content").String() + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.reasoning_content", oldVal+partTextResult.String()) } else { - oldVal := gjson.Get(choiceTemplate, "message.content").String() - choiceTemplate, _ = sjson.Set(choiceTemplate, "message.content", oldVal+partTextResult.String()) + oldVal := gjson.GetBytes(choiceTemplate, "message.content").String() + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.content", oldVal+partTextResult.String()) } - choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant") + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.role", "assistant") } else if functionCallResult.Exists() { // Append function call content to the tool_calls array. hasFunctionCall = true - toolCallsResult := gjson.Get(choiceTemplate, "message.tool_calls") + toolCallsResult := gjson.GetBytes(choiceTemplate, "message.tool_calls") if !toolCallsResult.Exists() || !toolCallsResult.IsArray() { - choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls", `[]`) + choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.tool_calls", []byte(`[]`)) } - functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}` + functionCallItemTemplate := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`) fcName := functionCallResult.Get("name").String() - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", fcName) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1))) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.name", fcName) if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() { - functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fcArgsResult.Raw) + functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", fcArgsResult.Raw) } - choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant") - choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls.-1", functionCallItemTemplate) + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.role", "assistant") + choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.tool_calls.-1", functionCallItemTemplate) } else if inlineDataResult.Exists() { data := inlineDataResult.Get("data").String() if data != "" { @@ -377,28 +377,28 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina mimeType = "image/png" } imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) - imagesResult := gjson.Get(choiceTemplate, "message.images") + imagesResult := gjson.GetBytes(choiceTemplate, "message.images") if !imagesResult.Exists() || !imagesResult.IsArray() { - choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.images", `[]`) + choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.images", []byte(`[]`)) } - imageIndex := len(gjson.Get(choiceTemplate, "message.images").Array()) - imagePayload := `{"type":"image_url","image_url":{"url":""}}` - imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex) - imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL) - choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant") - choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.images.-1", imagePayload) + imageIndex := len(gjson.GetBytes(choiceTemplate, "message.images").Array()) + imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex) + imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL) + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.role", "assistant") + choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.images.-1", imagePayload) } } } } if hasFunctionCall { - choiceTemplate, _ = sjson.Set(choiceTemplate, "finish_reason", "tool_calls") - choiceTemplate, _ = sjson.Set(choiceTemplate, "native_finish_reason", "tool_calls") + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "finish_reason", "tool_calls") + choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "native_finish_reason", "tool_calls") } // Append the constructed choice to the main choices array. - template, _ = sjson.SetRaw(template, "choices.-1", choiceTemplate) + template, _ = sjson.SetRawBytes(template, "choices.-1", choiceTemplate) return true }) } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go index 44b78346..b4754029 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_request.go @@ -19,15 +19,15 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte _ = stream // Unused but required by interface // Base Gemini API template (do not include thinkingConfig by default) - out := `{"contents":[]}` + out := []byte(`{"contents":[]}`) root := gjson.ParseBytes(rawJSON) // Extract system instruction from OpenAI "instructions" field if instructions := root.Get("instructions"); instructions.Exists() { - systemInstr := `{"parts":[{"text":""}]}` - systemInstr, _ = sjson.Set(systemInstr, "parts.0.text", instructions.String()) - out, _ = sjson.SetRaw(out, "systemInstruction", systemInstr) + systemInstr := []byte(`{"parts":[{"text":""}]}`) + systemInstr, _ = sjson.SetBytes(systemInstr, "parts.0.text", instructions.String()) + out, _ = sjson.SetRawBytes(out, "systemInstruction", systemInstr) } // Convert input messages to Gemini contents format @@ -78,8 +78,8 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte if len(calls) > 0 { outputMap := make(map[string]gjson.Result, len(outputs)) - for _, out := range outputs { - outputMap[out.Get("call_id").String()] = out + for _, outItem := range outputs { + outputMap[outItem.Get("call_id").String()] = outItem } for _, call := range calls { normalized = append(normalized, call) @@ -89,9 +89,9 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte delete(outputMap, callID) } } - for _, out := range outputs { - if _, ok := outputMap[out.Get("call_id").String()]; ok { - normalized = append(normalized, out) + for _, outItem := range outputs { + if _, ok := outputMap[outItem.Get("call_id").String()]; ok { + normalized = append(normalized, outItem) } } continue @@ -119,29 +119,27 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte case "message": if strings.EqualFold(itemRole, "system") { if contentArray := item.Get("content"); contentArray.Exists() { - systemInstr := "" - if systemInstructionResult := gjson.Get(out, "systemInstruction"); systemInstructionResult.Exists() { - systemInstr = systemInstructionResult.Raw - } else { - systemInstr = `{"parts":[]}` + systemInstr := []byte(`{"parts":[]}`) + if systemInstructionResult := gjson.GetBytes(out, "systemInstruction"); systemInstructionResult.Exists() { + systemInstr = []byte(systemInstructionResult.Raw) } if contentArray.IsArray() { contentArray.ForEach(func(_, contentItem gjson.Result) bool { - part := `{"text":""}` + part := []byte(`{"text":""}`) text := contentItem.Get("text").String() - part, _ = sjson.Set(part, "text", text) - systemInstr, _ = sjson.SetRaw(systemInstr, "parts.-1", part) + part, _ = sjson.SetBytes(part, "text", text) + systemInstr, _ = sjson.SetRawBytes(systemInstr, "parts.-1", part) return true }) } else if contentArray.Type == gjson.String { - part := `{"text":""}` - part, _ = sjson.Set(part, "text", contentArray.String()) - systemInstr, _ = sjson.SetRaw(systemInstr, "parts.-1", part) + part := []byte(`{"text":""}`) + part, _ = sjson.SetBytes(part, "text", contentArray.String()) + systemInstr, _ = sjson.SetRawBytes(systemInstr, "parts.-1", part) } - if systemInstr != `{"parts":[]}` { - out, _ = sjson.SetRaw(out, "systemInstruction", systemInstr) + if gjson.GetBytes(systemInstr, "parts.#").Int() > 0 { + out, _ = sjson.SetRawBytes(out, "systemInstruction", systemInstr) } } continue @@ -153,20 +151,20 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte // with roles derived from the content type to match docs/convert-2.md. if contentArray := item.Get("content"); contentArray.Exists() && contentArray.IsArray() { currentRole := "" - var currentParts []string + currentParts := make([][]byte, 0) flush := func() { if currentRole == "" || len(currentParts) == 0 { - currentParts = nil + currentParts = currentParts[:0] return } - one := `{"role":"","parts":[]}` - one, _ = sjson.Set(one, "role", currentRole) + one := []byte(`{"role":"","parts":[]}`) + one, _ = sjson.SetBytes(one, "role", currentRole) for _, part := range currentParts { - one, _ = sjson.SetRaw(one, "parts.-1", part) + one, _ = sjson.SetRawBytes(one, "parts.-1", part) } - out, _ = sjson.SetRaw(out, "contents.-1", one) - currentParts = nil + out, _ = sjson.SetRawBytes(out, "contents.-1", one) + currentParts = currentParts[:0] } contentArray.ForEach(func(_, contentItem gjson.Result) bool { @@ -199,12 +197,12 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte currentRole = effRole } - var partJSON string + var partJSON []byte switch contentType { case "input_text", "output_text": if text := contentItem.Get("text"); text.Exists() { - partJSON = `{"text":""}` - partJSON, _ = sjson.Set(partJSON, "text", text.String()) + partJSON = []byte(`{"text":""}`) + partJSON, _ = sjson.SetBytes(partJSON, "text", text.String()) } case "input_image": imageURL := contentItem.Get("image_url").String() @@ -233,9 +231,9 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte } } if data != "" { - partJSON = `{"inline_data":{"mime_type":"","data":""}}` - partJSON, _ = sjson.Set(partJSON, "inline_data.mime_type", mimeType) - partJSON, _ = sjson.Set(partJSON, "inline_data.data", data) + partJSON = []byte(`{"inline_data":{"mime_type":"","data":""}}`) + partJSON, _ = sjson.SetBytes(partJSON, "inline_data.mime_type", mimeType) + partJSON, _ = sjson.SetBytes(partJSON, "inline_data.data", data) } } case "input_audio": @@ -261,13 +259,13 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte mimeType = "audio/" + audioFormat } } - partJSON = `{"inline_data":{"mime_type":"","data":""}}` - partJSON, _ = sjson.Set(partJSON, "inline_data.mime_type", mimeType) - partJSON, _ = sjson.Set(partJSON, "inline_data.data", audioData) + partJSON = []byte(`{"inline_data":{"mime_type":"","data":""}}`) + partJSON, _ = sjson.SetBytes(partJSON, "inline_data.mime_type", mimeType) + partJSON, _ = sjson.SetBytes(partJSON, "inline_data.data", audioData) } } - if partJSON != "" { + if len(partJSON) > 0 { currentParts = append(currentParts, partJSON) } return true @@ -285,30 +283,31 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte } } - one := `{"role":"","parts":[{"text":""}]}` - one, _ = sjson.Set(one, "role", effRole) - one, _ = sjson.Set(one, "parts.0.text", contentArray.String()) - out, _ = sjson.SetRaw(out, "contents.-1", one) + one := []byte(`{"role":"","parts":[{"text":""}]}`) + one, _ = sjson.SetBytes(one, "role", effRole) + one, _ = sjson.SetBytes(one, "parts.0.text", contentArray.String()) + out, _ = sjson.SetRawBytes(out, "contents.-1", one) } + case "function_call": // Handle function calls - convert to model message with functionCall name := item.Get("name").String() arguments := item.Get("arguments").String() - modelContent := `{"role":"model","parts":[]}` - functionCall := `{"functionCall":{"name":"","args":{}}}` - functionCall, _ = sjson.Set(functionCall, "functionCall.name", name) - functionCall, _ = sjson.Set(functionCall, "thoughtSignature", geminiResponsesThoughtSignature) - functionCall, _ = sjson.Set(functionCall, "functionCall.id", item.Get("call_id").String()) + modelContent := []byte(`{"role":"model","parts":[]}`) + functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`) + functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", name) + functionCall, _ = sjson.SetBytes(functionCall, "thoughtSignature", geminiResponsesThoughtSignature) + functionCall, _ = sjson.SetBytes(functionCall, "functionCall.id", item.Get("call_id").String()) // Parse arguments JSON string and set as args object if arguments != "" { argsResult := gjson.Parse(arguments) - functionCall, _ = sjson.SetRaw(functionCall, "functionCall.args", argsResult.Raw) + functionCall, _ = sjson.SetRawBytes(functionCall, "functionCall.args", []byte(argsResult.Raw)) } - modelContent, _ = sjson.SetRaw(modelContent, "parts.-1", functionCall) - out, _ = sjson.SetRaw(out, "contents.-1", modelContent) + modelContent, _ = sjson.SetRawBytes(modelContent, "parts.-1", functionCall) + out, _ = sjson.SetRawBytes(out, "contents.-1", modelContent) case "function_call_output": // Handle function call outputs - convert to function message with functionResponse @@ -316,8 +315,8 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte // Use .Raw to preserve the JSON encoding (includes quotes for strings) outputRaw := item.Get("output").Str - functionContent := `{"role":"function","parts":[]}` - functionResponse := `{"functionResponse":{"name":"","response":{}}}` + functionContent := []byte(`{"role":"function","parts":[]}`) + functionResponse := []byte(`{"functionResponse":{"name":"","response":{}}}`) // We need to extract the function name from the previous function_call // For now, we'll use a placeholder or extract from context if available @@ -335,101 +334,101 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte }) } - functionResponse, _ = sjson.Set(functionResponse, "functionResponse.name", functionName) - functionResponse, _ = sjson.Set(functionResponse, "functionResponse.id", callID) + functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.name", functionName) + functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.id", callID) // Set the raw JSON output directly (preserves string encoding) if outputRaw != "" && outputRaw != "null" { output := gjson.Parse(outputRaw) if output.Type == gjson.JSON && json.Valid([]byte(output.Raw)) { - functionResponse, _ = sjson.SetRaw(functionResponse, "functionResponse.response.result", output.Raw) + functionResponse, _ = sjson.SetRawBytes(functionResponse, "functionResponse.response.result", []byte(output.Raw)) } else { - functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw) + functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.response.result", outputRaw) } } - functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse) - out, _ = sjson.SetRaw(out, "contents.-1", functionContent) + functionContent, _ = sjson.SetRawBytes(functionContent, "parts.-1", functionResponse) + out, _ = sjson.SetRawBytes(out, "contents.-1", functionContent) case "reasoning": - thoughtContent := `{"role":"model","parts":[]}` - thought := `{"text":"","thoughtSignature":"","thought":true}` - thought, _ = sjson.Set(thought, "text", item.Get("summary.0.text").String()) - thought, _ = sjson.Set(thought, "thoughtSignature", item.Get("encrypted_content").String()) + thoughtContent := []byte(`{"role":"model","parts":[]}`) + thought := []byte(`{"text":"","thoughtSignature":"","thought":true}`) + thought, _ = sjson.SetBytes(thought, "text", item.Get("summary.0.text").String()) + thought, _ = sjson.SetBytes(thought, "thoughtSignature", item.Get("encrypted_content").String()) - thoughtContent, _ = sjson.SetRaw(thoughtContent, "parts.-1", thought) - out, _ = sjson.SetRaw(out, "contents.-1", thoughtContent) + thoughtContent, _ = sjson.SetRawBytes(thoughtContent, "parts.-1", thought) + out, _ = sjson.SetRawBytes(out, "contents.-1", thoughtContent) } } } else if input.Exists() && input.Type == gjson.String { // Simple string input conversion to user message - userContent := `{"role":"user","parts":[{"text":""}]}` - userContent, _ = sjson.Set(userContent, "parts.0.text", input.String()) - out, _ = sjson.SetRaw(out, "contents.-1", userContent) + userContent := []byte(`{"role":"user","parts":[{"text":""}]}`) + userContent, _ = sjson.SetBytes(userContent, "parts.0.text", input.String()) + out, _ = sjson.SetRawBytes(out, "contents.-1", userContent) } // Convert tools to Gemini functionDeclarations format if tools := root.Get("tools"); tools.Exists() && tools.IsArray() { - geminiTools := `[{"functionDeclarations":[]}]` + geminiTools := []byte(`[{"functionDeclarations":[]}]`) tools.ForEach(func(_, tool gjson.Result) bool { if tool.Get("type").String() == "function" { - funcDecl := `{"name":"","description":"","parametersJsonSchema":{}}` + funcDecl := []byte(`{"name":"","description":"","parametersJsonSchema":{}}`) if name := tool.Get("name"); name.Exists() { - funcDecl, _ = sjson.Set(funcDecl, "name", name.String()) + funcDecl, _ = sjson.SetBytes(funcDecl, "name", name.String()) } if desc := tool.Get("description"); desc.Exists() { - funcDecl, _ = sjson.Set(funcDecl, "description", desc.String()) + funcDecl, _ = sjson.SetBytes(funcDecl, "description", desc.String()) } if params := tool.Get("parameters"); params.Exists() { - funcDecl, _ = sjson.SetRaw(funcDecl, "parametersJsonSchema", params.Raw) + funcDecl, _ = sjson.SetRawBytes(funcDecl, "parametersJsonSchema", []byte(params.Raw)) } - geminiTools, _ = sjson.SetRaw(geminiTools, "0.functionDeclarations.-1", funcDecl) + geminiTools, _ = sjson.SetRawBytes(geminiTools, "0.functionDeclarations.-1", funcDecl) } return true }) // Only add tools if there are function declarations - if funcDecls := gjson.Get(geminiTools, "0.functionDeclarations"); funcDecls.Exists() && len(funcDecls.Array()) > 0 { - out, _ = sjson.SetRaw(out, "tools", geminiTools) + if funcDecls := gjson.GetBytes(geminiTools, "0.functionDeclarations"); funcDecls.Exists() && len(funcDecls.Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "tools", geminiTools) } } // Handle generation config from OpenAI format if maxOutputTokens := root.Get("max_output_tokens"); maxOutputTokens.Exists() { - genConfig := `{"maxOutputTokens":0}` - genConfig, _ = sjson.Set(genConfig, "maxOutputTokens", maxOutputTokens.Int()) - out, _ = sjson.SetRaw(out, "generationConfig", genConfig) + genConfig := []byte(`{"maxOutputTokens":0}`) + genConfig, _ = sjson.SetBytes(genConfig, "maxOutputTokens", maxOutputTokens.Int()) + out, _ = sjson.SetRawBytes(out, "generationConfig", genConfig) } // Handle temperature if present if temperature := root.Get("temperature"); temperature.Exists() { - if !gjson.Get(out, "generationConfig").Exists() { - out, _ = sjson.SetRaw(out, "generationConfig", `{}`) + if !gjson.GetBytes(out, "generationConfig").Exists() { + out, _ = sjson.SetRawBytes(out, "generationConfig", []byte(`{}`)) } - out, _ = sjson.Set(out, "generationConfig.temperature", temperature.Float()) + out, _ = sjson.SetBytes(out, "generationConfig.temperature", temperature.Float()) } // Handle top_p if present if topP := root.Get("top_p"); topP.Exists() { - if !gjson.Get(out, "generationConfig").Exists() { - out, _ = sjson.SetRaw(out, "generationConfig", `{}`) + if !gjson.GetBytes(out, "generationConfig").Exists() { + out, _ = sjson.SetRawBytes(out, "generationConfig", []byte(`{}`)) } - out, _ = sjson.Set(out, "generationConfig.topP", topP.Float()) + out, _ = sjson.SetBytes(out, "generationConfig.topP", topP.Float()) } // Handle stop sequences if stopSequences := root.Get("stop_sequences"); stopSequences.Exists() && stopSequences.IsArray() { - if !gjson.Get(out, "generationConfig").Exists() { - out, _ = sjson.SetRaw(out, "generationConfig", `{}`) + if !gjson.GetBytes(out, "generationConfig").Exists() { + out, _ = sjson.SetRawBytes(out, "generationConfig", []byte(`{}`)) } var sequences []string stopSequences.ForEach(func(_, seq gjson.Result) bool { sequences = append(sequences, seq.String()) return true }) - out, _ = sjson.Set(out, "generationConfig.stopSequences", sequences) + out, _ = sjson.SetBytes(out, "generationConfig.stopSequences", sequences) } // Apply thinking configuration: convert OpenAI Responses API reasoning.effort to Gemini thinkingConfig. @@ -440,16 +439,16 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte if effort != "" { thinkingPath := "generationConfig.thinkingConfig" if effort == "auto" { - out, _ = sjson.Set(out, thinkingPath+".thinkingBudget", -1) - out, _ = sjson.Set(out, thinkingPath+".includeThoughts", true) + out, _ = sjson.SetBytes(out, thinkingPath+".thinkingBudget", -1) + out, _ = sjson.SetBytes(out, thinkingPath+".includeThoughts", true) } else { - out, _ = sjson.Set(out, thinkingPath+".thinkingLevel", effort) - out, _ = sjson.Set(out, thinkingPath+".includeThoughts", effort != "none") + out, _ = sjson.SetBytes(out, thinkingPath+".thinkingLevel", effort) + out, _ = sjson.SetBytes(out, thinkingPath+".includeThoughts", effort != "none") } } } - result := []byte(out) + result := out result = common.AttachDefaultSafetySettings(result, "safetySettings") return result } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go index 73609be7..30e50325 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_response.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -81,12 +82,12 @@ func unwrapGeminiResponseRoot(root gjson.Result) gjson.Result { return root } -func emitEvent(event string, payload string) string { - return fmt.Sprintf("event: %s\ndata: %s", event, payload) +func emitEvent(event string, payload []byte) []byte { + return translatorcommon.SSEEventData(event, payload) } // ConvertGeminiResponseToOpenAIResponses converts Gemini SSE chunks into OpenAI Responses SSE events. -func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &geminiToResponsesState{ FuncArgsBuf: make(map[int]*strings.Builder), @@ -115,16 +116,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, rawJSON = bytes.TrimSpace(rawJSON) if len(rawJSON) == 0 || bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } root := gjson.ParseBytes(rawJSON) if !root.Exists() { - return []string{} + return [][]byte{} } root = unwrapGeminiResponseRoot(root) - var out []string + var out [][]byte nextSeq := func() int { st.Seq++; return st.Seq } // Helper to finalize reasoning summary events in correct order. @@ -135,26 +136,26 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, return } full := st.ReasoningBuf.String() - textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}` - textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq()) - textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningItemID) - textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex) - textDone, _ = sjson.Set(textDone, "text", full) + textDone := []byte(`{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`) + textDone, _ = sjson.SetBytes(textDone, "sequence_number", nextSeq()) + textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningItemID) + textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex) + textDone, _ = sjson.SetBytes(textDone, "text", full) out = append(out, emitEvent("response.reasoning_summary_text.done", textDone)) - partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningItemID) - partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex) - partDone, _ = sjson.Set(partDone, "part.text", full) + partDone := []byte(`{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningItemID) + partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex) + partDone, _ = sjson.SetBytes(partDone, "part.text", full) out = append(out, emitEvent("response.reasoning_summary_part.done", partDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "item.id", st.ReasoningItemID) - itemDone, _ = sjson.Set(itemDone, "output_index", st.ReasoningIndex) - itemDone, _ = sjson.Set(itemDone, "item.encrypted_content", st.ReasoningEnc) - itemDone, _ = sjson.Set(itemDone, "item.summary.0.text", full) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", st.ReasoningItemID) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", st.ReasoningIndex) + itemDone, _ = sjson.SetBytes(itemDone, "item.encrypted_content", st.ReasoningEnc) + itemDone, _ = sjson.SetBytes(itemDone, "item.summary.0.text", full) out = append(out, emitEvent("response.output_item.done", itemDone)) st.ReasoningClosed = true @@ -168,23 +169,23 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, return } fullText := st.ItemTextBuf.String() - done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}` - done, _ = sjson.Set(done, "sequence_number", nextSeq()) - done, _ = sjson.Set(done, "item_id", st.CurrentMsgID) - done, _ = sjson.Set(done, "output_index", st.MsgIndex) - done, _ = sjson.Set(done, "text", fullText) + done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) + done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) + done, _ = sjson.SetBytes(done, "item_id", st.CurrentMsgID) + done, _ = sjson.SetBytes(done, "output_index", st.MsgIndex) + done, _ = sjson.SetBytes(done, "text", fullText) out = append(out, emitEvent("response.output_text.done", done)) - partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", st.CurrentMsgID) - partDone, _ = sjson.Set(partDone, "output_index", st.MsgIndex) - partDone, _ = sjson.Set(partDone, "part.text", fullText) + partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", st.CurrentMsgID) + partDone, _ = sjson.SetBytes(partDone, "output_index", st.MsgIndex) + partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) out = append(out, emitEvent("response.content_part.done", partDone)) - final := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","text":""}],"role":"assistant"}}` - final, _ = sjson.Set(final, "sequence_number", nextSeq()) - final, _ = sjson.Set(final, "output_index", st.MsgIndex) - final, _ = sjson.Set(final, "item.id", st.CurrentMsgID) - final, _ = sjson.Set(final, "item.content.0.text", fullText) + final := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","text":""}],"role":"assistant"}}`) + final, _ = sjson.SetBytes(final, "sequence_number", nextSeq()) + final, _ = sjson.SetBytes(final, "output_index", st.MsgIndex) + final, _ = sjson.SetBytes(final, "item.id", st.CurrentMsgID) + final, _ = sjson.SetBytes(final, "item.content.0.text", fullText) out = append(out, emitEvent("response.output_item.done", final)) st.MsgClosed = true @@ -208,16 +209,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, st.CreatedAt = time.Now().Unix() } - created := `{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}` - created, _ = sjson.Set(created, "sequence_number", nextSeq()) - created, _ = sjson.Set(created, "response.id", st.ResponseID) - created, _ = sjson.Set(created, "response.created_at", st.CreatedAt) + created := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`) + created, _ = sjson.SetBytes(created, "sequence_number", nextSeq()) + created, _ = sjson.SetBytes(created, "response.id", st.ResponseID) + created, _ = sjson.SetBytes(created, "response.created_at", st.CreatedAt) out = append(out, emitEvent("response.created", created)) - inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}` - inprog, _ = sjson.Set(inprog, "sequence_number", nextSeq()) - inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID) - inprog, _ = sjson.Set(inprog, "response.created_at", st.CreatedAt) + inprog := []byte(`{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`) + inprog, _ = sjson.SetBytes(inprog, "sequence_number", nextSeq()) + inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID) + inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.CreatedAt) out = append(out, emitEvent("response.in_progress", inprog)) st.Started = true @@ -243,25 +244,25 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, st.ReasoningIndex = st.NextIndex st.NextIndex++ st.ReasoningItemID = fmt.Sprintf("rs_%s_%d", st.ResponseID, st.ReasoningIndex) - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","encrypted_content":"","summary":[]}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", st.ReasoningIndex) - item, _ = sjson.Set(item, "item.id", st.ReasoningItemID) - item, _ = sjson.Set(item, "item.encrypted_content", st.ReasoningEnc) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","encrypted_content":"","summary":[]}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", st.ReasoningIndex) + item, _ = sjson.SetBytes(item, "item.id", st.ReasoningItemID) + item, _ = sjson.SetBytes(item, "item.encrypted_content", st.ReasoningEnc) out = append(out, emitEvent("response.output_item.added", item)) - partAdded := `{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}` - partAdded, _ = sjson.Set(partAdded, "sequence_number", nextSeq()) - partAdded, _ = sjson.Set(partAdded, "item_id", st.ReasoningItemID) - partAdded, _ = sjson.Set(partAdded, "output_index", st.ReasoningIndex) + partAdded := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) + partAdded, _ = sjson.SetBytes(partAdded, "sequence_number", nextSeq()) + partAdded, _ = sjson.SetBytes(partAdded, "item_id", st.ReasoningItemID) + partAdded, _ = sjson.SetBytes(partAdded, "output_index", st.ReasoningIndex) out = append(out, emitEvent("response.reasoning_summary_part.added", partAdded)) } if t := part.Get("text"); t.Exists() && t.String() != "" { st.ReasoningBuf.WriteString(t.String()) - msg := `{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", st.ReasoningItemID) - msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex) - msg, _ = sjson.Set(msg, "delta", t.String()) + msg := []byte(`{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningItemID) + msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex) + msg, _ = sjson.SetBytes(msg, "delta", t.String()) out = append(out, emitEvent("response.reasoning_summary_text.delta", msg)) } return true @@ -276,25 +277,25 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, st.MsgIndex = st.NextIndex st.NextIndex++ st.CurrentMsgID = fmt.Sprintf("msg_%s_0", st.ResponseID) - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", st.MsgIndex) - item, _ = sjson.Set(item, "item.id", st.CurrentMsgID) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", st.MsgIndex) + item, _ = sjson.SetBytes(item, "item.id", st.CurrentMsgID) out = append(out, emitEvent("response.output_item.added", item)) - partAdded := `{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - partAdded, _ = sjson.Set(partAdded, "sequence_number", nextSeq()) - partAdded, _ = sjson.Set(partAdded, "item_id", st.CurrentMsgID) - partAdded, _ = sjson.Set(partAdded, "output_index", st.MsgIndex) + partAdded := []byte(`{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + partAdded, _ = sjson.SetBytes(partAdded, "sequence_number", nextSeq()) + partAdded, _ = sjson.SetBytes(partAdded, "item_id", st.CurrentMsgID) + partAdded, _ = sjson.SetBytes(partAdded, "output_index", st.MsgIndex) out = append(out, emitEvent("response.content_part.added", partAdded)) st.ItemTextBuf.Reset() } st.TextBuf.WriteString(t.String()) st.ItemTextBuf.WriteString(t.String()) - msg := `{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", st.CurrentMsgID) - msg, _ = sjson.Set(msg, "output_index", st.MsgIndex) - msg, _ = sjson.Set(msg, "delta", t.String()) + msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", st.CurrentMsgID) + msg, _ = sjson.SetBytes(msg, "output_index", st.MsgIndex) + msg, _ = sjson.SetBytes(msg, "delta", t.String()) out = append(out, emitEvent("response.output_text.delta", msg)) return true } @@ -326,41 +327,41 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, } // Emit item.added for function call - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", idx) - item, _ = sjson.Set(item, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) - item, _ = sjson.Set(item, "item.call_id", st.FuncCallIDs[idx]) - item, _ = sjson.Set(item, "item.name", name) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) + item, _ = sjson.SetBytes(item, "item.call_id", st.FuncCallIDs[idx]) + item, _ = sjson.SetBytes(item, "item.name", name) out = append(out, emitEvent("response.output_item.added", item)) // Emit arguments delta (full args in one chunk). // When Gemini omits args, emit "{}" to keep Responses streaming event order consistent. if argsJSON != "" { - ad := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}` - ad, _ = sjson.Set(ad, "sequence_number", nextSeq()) - ad, _ = sjson.Set(ad, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) - ad, _ = sjson.Set(ad, "output_index", idx) - ad, _ = sjson.Set(ad, "delta", argsJSON) + ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) + ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq()) + ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) + ad, _ = sjson.SetBytes(ad, "output_index", idx) + ad, _ = sjson.SetBytes(ad, "delta", argsJSON) out = append(out, emitEvent("response.function_call_arguments.delta", ad)) } // Gemini emits the full function call payload at once, so we can finalize it immediately. if !st.FuncDone[idx] { - fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}` - fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq()) - fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) - fcDone, _ = sjson.Set(fcDone, "output_index", idx) - fcDone, _ = sjson.Set(fcDone, "arguments", argsJSON) + fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`) + fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq()) + fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) + fcDone, _ = sjson.SetBytes(fcDone, "output_index", idx) + fcDone, _ = sjson.SetBytes(fcDone, "arguments", argsJSON) out = append(out, emitEvent("response.function_call_arguments.done", fcDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "output_index", idx) - itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) - itemDone, _ = sjson.Set(itemDone, "item.arguments", argsJSON) - itemDone, _ = sjson.Set(itemDone, "item.call_id", st.FuncCallIDs[idx]) - itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[idx]) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) + itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", argsJSON) + itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", st.FuncCallIDs[idx]) + itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[idx]) out = append(out, emitEvent("response.output_item.done", itemDone)) st.FuncDone[idx] = true @@ -401,20 +402,20 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, if b := st.FuncArgsBuf[idx]; b != nil && b.Len() > 0 { args = b.String() } - fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}` - fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq()) - fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) - fcDone, _ = sjson.Set(fcDone, "output_index", idx) - fcDone, _ = sjson.Set(fcDone, "arguments", args) + fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`) + fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq()) + fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) + fcDone, _ = sjson.SetBytes(fcDone, "output_index", idx) + fcDone, _ = sjson.SetBytes(fcDone, "arguments", args) out = append(out, emitEvent("response.function_call_arguments.done", fcDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "output_index", idx) - itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) - itemDone, _ = sjson.Set(itemDone, "item.arguments", args) - itemDone, _ = sjson.Set(itemDone, "item.call_id", st.FuncCallIDs[idx]) - itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[idx]) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx])) + itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args) + itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", st.FuncCallIDs[idx]) + itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[idx]) out = append(out, emitEvent("response.output_item.done", itemDone)) st.FuncDone[idx] = true @@ -424,91 +425,91 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, // Reasoning already finalized above if present // Build response.completed with aggregated outputs and request echo fields - completed := `{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}` - completed, _ = sjson.Set(completed, "sequence_number", nextSeq()) - completed, _ = sjson.Set(completed, "response.id", st.ResponseID) - completed, _ = sjson.Set(completed, "response.created_at", st.CreatedAt) + completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`) + completed, _ = sjson.SetBytes(completed, "sequence_number", nextSeq()) + completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID) + completed, _ = sjson.SetBytes(completed, "response.created_at", st.CreatedAt) if reqJSON := pickRequestJSON(originalRequestRawJSON, requestRawJSON); len(reqJSON) > 0 { req := unwrapRequestRoot(gjson.ParseBytes(reqJSON)) if v := req.Get("instructions"); v.Exists() { - completed, _ = sjson.Set(completed, "response.instructions", v.String()) + completed, _ = sjson.SetBytes(completed, "response.instructions", v.String()) } if v := req.Get("max_output_tokens"); v.Exists() { - completed, _ = sjson.Set(completed, "response.max_output_tokens", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.max_output_tokens", v.Int()) } if v := req.Get("max_tool_calls"); v.Exists() { - completed, _ = sjson.Set(completed, "response.max_tool_calls", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.max_tool_calls", v.Int()) } if v := req.Get("model"); v.Exists() { - completed, _ = sjson.Set(completed, "response.model", v.String()) + completed, _ = sjson.SetBytes(completed, "response.model", v.String()) } if v := req.Get("parallel_tool_calls"); v.Exists() { - completed, _ = sjson.Set(completed, "response.parallel_tool_calls", v.Bool()) + completed, _ = sjson.SetBytes(completed, "response.parallel_tool_calls", v.Bool()) } if v := req.Get("previous_response_id"); v.Exists() { - completed, _ = sjson.Set(completed, "response.previous_response_id", v.String()) + completed, _ = sjson.SetBytes(completed, "response.previous_response_id", v.String()) } if v := req.Get("prompt_cache_key"); v.Exists() { - completed, _ = sjson.Set(completed, "response.prompt_cache_key", v.String()) + completed, _ = sjson.SetBytes(completed, "response.prompt_cache_key", v.String()) } if v := req.Get("reasoning"); v.Exists() { - completed, _ = sjson.Set(completed, "response.reasoning", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.reasoning", v.Value()) } if v := req.Get("safety_identifier"); v.Exists() { - completed, _ = sjson.Set(completed, "response.safety_identifier", v.String()) + completed, _ = sjson.SetBytes(completed, "response.safety_identifier", v.String()) } if v := req.Get("service_tier"); v.Exists() { - completed, _ = sjson.Set(completed, "response.service_tier", v.String()) + completed, _ = sjson.SetBytes(completed, "response.service_tier", v.String()) } if v := req.Get("store"); v.Exists() { - completed, _ = sjson.Set(completed, "response.store", v.Bool()) + completed, _ = sjson.SetBytes(completed, "response.store", v.Bool()) } if v := req.Get("temperature"); v.Exists() { - completed, _ = sjson.Set(completed, "response.temperature", v.Float()) + completed, _ = sjson.SetBytes(completed, "response.temperature", v.Float()) } if v := req.Get("text"); v.Exists() { - completed, _ = sjson.Set(completed, "response.text", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.text", v.Value()) } if v := req.Get("tool_choice"); v.Exists() { - completed, _ = sjson.Set(completed, "response.tool_choice", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.tool_choice", v.Value()) } if v := req.Get("tools"); v.Exists() { - completed, _ = sjson.Set(completed, "response.tools", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.tools", v.Value()) } if v := req.Get("top_logprobs"); v.Exists() { - completed, _ = sjson.Set(completed, "response.top_logprobs", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.top_logprobs", v.Int()) } if v := req.Get("top_p"); v.Exists() { - completed, _ = sjson.Set(completed, "response.top_p", v.Float()) + completed, _ = sjson.SetBytes(completed, "response.top_p", v.Float()) } if v := req.Get("truncation"); v.Exists() { - completed, _ = sjson.Set(completed, "response.truncation", v.String()) + completed, _ = sjson.SetBytes(completed, "response.truncation", v.String()) } if v := req.Get("user"); v.Exists() { - completed, _ = sjson.Set(completed, "response.user", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.user", v.Value()) } if v := req.Get("metadata"); v.Exists() { - completed, _ = sjson.Set(completed, "response.metadata", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.metadata", v.Value()) } } // Compose outputs in output_index order. - outputsWrapper := `{"arr":[]}` + outputsWrapper := []byte(`{"arr":[]}`) for idx := 0; idx < st.NextIndex; idx++ { if st.ReasoningOpened && idx == st.ReasoningIndex { - item := `{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]}` - item, _ = sjson.Set(item, "id", st.ReasoningItemID) - item, _ = sjson.Set(item, "encrypted_content", st.ReasoningEnc) - item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]}`) + item, _ = sjson.SetBytes(item, "id", st.ReasoningItemID) + item, _ = sjson.SetBytes(item, "encrypted_content", st.ReasoningEnc) + item, _ = sjson.SetBytes(item, "summary.0.text", st.ReasoningBuf.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) continue } if st.MsgOpened && idx == st.MsgIndex { - item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` - item, _ = sjson.Set(item, "id", st.CurrentMsgID) - item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) + item, _ = sjson.SetBytes(item, "id", st.CurrentMsgID) + item, _ = sjson.SetBytes(item, "content.0.text", st.TextBuf.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) continue } @@ -517,40 +518,40 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, if b := st.FuncArgsBuf[idx]; b != nil && b.Len() > 0 { args = b.String() } - item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) - item, _ = sjson.Set(item, "arguments", args) - item, _ = sjson.Set(item, "call_id", callID) - item, _ = sjson.Set(item, "name", st.FuncNames[idx]) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID)) + item, _ = sjson.SetBytes(item, "arguments", args) + item, _ = sjson.SetBytes(item, "call_id", callID) + item, _ = sjson.SetBytes(item, "name", st.FuncNames[idx]) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } } - if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { - completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw) + if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { + completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) } // usage mapping if um := root.Get("usageMetadata"); um.Exists() { // input tokens = prompt only (thoughts go to output) input := um.Get("promptTokenCount").Int() - completed, _ = sjson.Set(completed, "response.usage.input_tokens", input) + completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", input) // cached token details: align with OpenAI "cached_tokens" semantics. - completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", um.Get("cachedContentTokenCount").Int()) + completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens_details.cached_tokens", um.Get("cachedContentTokenCount").Int()) // output tokens if v := um.Get("candidatesTokenCount"); v.Exists() { - completed, _ = sjson.Set(completed, "response.usage.output_tokens", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens", v.Int()) } else { - completed, _ = sjson.Set(completed, "response.usage.output_tokens", 0) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens", 0) } if v := um.Get("thoughtsTokenCount"); v.Exists() { - completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens_details.reasoning_tokens", v.Int()) } else { - completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", 0) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens_details.reasoning_tokens", 0) } if v := um.Get("totalTokenCount"); v.Exists() { - completed, _ = sjson.Set(completed, "response.usage.total_tokens", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.usage.total_tokens", v.Int()) } else { - completed, _ = sjson.Set(completed, "response.usage.total_tokens", 0) + completed, _ = sjson.SetBytes(completed, "response.usage.total_tokens", 0) } } @@ -561,12 +562,12 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string, } // ConvertGeminiResponseToOpenAIResponsesNonStream aggregates Gemini response JSON into a single OpenAI Responses JSON object. -func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { root := gjson.ParseBytes(rawJSON) root = unwrapGeminiResponseRoot(root) // Base response scaffold - resp := `{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}` + resp := []byte(`{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`) // id: prefer provider responseId, otherwise synthesize id := root.Get("responseId").String() @@ -577,7 +578,7 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string if !strings.HasPrefix(id, "resp_") { id = fmt.Sprintf("resp_%s", id) } - resp, _ = sjson.Set(resp, "id", id) + resp, _ = sjson.SetBytes(resp, "id", id) // created_at: map from createTime if available createdAt := time.Now().Unix() @@ -586,75 +587,75 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string createdAt = t.Unix() } } - resp, _ = sjson.Set(resp, "created_at", createdAt) + resp, _ = sjson.SetBytes(resp, "created_at", createdAt) // Echo request fields when present; fallback model from response modelVersion if reqJSON := pickRequestJSON(originalRequestRawJSON, requestRawJSON); len(reqJSON) > 0 { req := unwrapRequestRoot(gjson.ParseBytes(reqJSON)) if v := req.Get("instructions"); v.Exists() { - resp, _ = sjson.Set(resp, "instructions", v.String()) + resp, _ = sjson.SetBytes(resp, "instructions", v.String()) } if v := req.Get("max_output_tokens"); v.Exists() { - resp, _ = sjson.Set(resp, "max_output_tokens", v.Int()) + resp, _ = sjson.SetBytes(resp, "max_output_tokens", v.Int()) } if v := req.Get("max_tool_calls"); v.Exists() { - resp, _ = sjson.Set(resp, "max_tool_calls", v.Int()) + resp, _ = sjson.SetBytes(resp, "max_tool_calls", v.Int()) } if v := req.Get("model"); v.Exists() { - resp, _ = sjson.Set(resp, "model", v.String()) + resp, _ = sjson.SetBytes(resp, "model", v.String()) } else if v = root.Get("modelVersion"); v.Exists() { - resp, _ = sjson.Set(resp, "model", v.String()) + resp, _ = sjson.SetBytes(resp, "model", v.String()) } if v := req.Get("parallel_tool_calls"); v.Exists() { - resp, _ = sjson.Set(resp, "parallel_tool_calls", v.Bool()) + resp, _ = sjson.SetBytes(resp, "parallel_tool_calls", v.Bool()) } if v := req.Get("previous_response_id"); v.Exists() { - resp, _ = sjson.Set(resp, "previous_response_id", v.String()) + resp, _ = sjson.SetBytes(resp, "previous_response_id", v.String()) } if v := req.Get("prompt_cache_key"); v.Exists() { - resp, _ = sjson.Set(resp, "prompt_cache_key", v.String()) + resp, _ = sjson.SetBytes(resp, "prompt_cache_key", v.String()) } if v := req.Get("reasoning"); v.Exists() { - resp, _ = sjson.Set(resp, "reasoning", v.Value()) + resp, _ = sjson.SetBytes(resp, "reasoning", v.Value()) } if v := req.Get("safety_identifier"); v.Exists() { - resp, _ = sjson.Set(resp, "safety_identifier", v.String()) + resp, _ = sjson.SetBytes(resp, "safety_identifier", v.String()) } if v := req.Get("service_tier"); v.Exists() { - resp, _ = sjson.Set(resp, "service_tier", v.String()) + resp, _ = sjson.SetBytes(resp, "service_tier", v.String()) } if v := req.Get("store"); v.Exists() { - resp, _ = sjson.Set(resp, "store", v.Bool()) + resp, _ = sjson.SetBytes(resp, "store", v.Bool()) } if v := req.Get("temperature"); v.Exists() { - resp, _ = sjson.Set(resp, "temperature", v.Float()) + resp, _ = sjson.SetBytes(resp, "temperature", v.Float()) } if v := req.Get("text"); v.Exists() { - resp, _ = sjson.Set(resp, "text", v.Value()) + resp, _ = sjson.SetBytes(resp, "text", v.Value()) } if v := req.Get("tool_choice"); v.Exists() { - resp, _ = sjson.Set(resp, "tool_choice", v.Value()) + resp, _ = sjson.SetBytes(resp, "tool_choice", v.Value()) } if v := req.Get("tools"); v.Exists() { - resp, _ = sjson.Set(resp, "tools", v.Value()) + resp, _ = sjson.SetBytes(resp, "tools", v.Value()) } if v := req.Get("top_logprobs"); v.Exists() { - resp, _ = sjson.Set(resp, "top_logprobs", v.Int()) + resp, _ = sjson.SetBytes(resp, "top_logprobs", v.Int()) } if v := req.Get("top_p"); v.Exists() { - resp, _ = sjson.Set(resp, "top_p", v.Float()) + resp, _ = sjson.SetBytes(resp, "top_p", v.Float()) } if v := req.Get("truncation"); v.Exists() { - resp, _ = sjson.Set(resp, "truncation", v.String()) + resp, _ = sjson.SetBytes(resp, "truncation", v.String()) } if v := req.Get("user"); v.Exists() { - resp, _ = sjson.Set(resp, "user", v.Value()) + resp, _ = sjson.SetBytes(resp, "user", v.Value()) } if v := req.Get("metadata"); v.Exists() { - resp, _ = sjson.Set(resp, "metadata", v.Value()) + resp, _ = sjson.SetBytes(resp, "metadata", v.Value()) } } else if v := root.Get("modelVersion"); v.Exists() { - resp, _ = sjson.Set(resp, "model", v.String()) + resp, _ = sjson.SetBytes(resp, "model", v.String()) } // Build outputs from candidates[0].content.parts @@ -668,12 +669,12 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string if haveOutput { return } - resp, _ = sjson.SetRaw(resp, "output", "[]") + resp, _ = sjson.SetRawBytes(resp, "output", []byte("[]")) haveOutput = true } - appendOutput := func(itemJSON string) { + appendOutput := func(itemJSON []byte) { ensureOutput() - resp, _ = sjson.SetRaw(resp, "output.-1", itemJSON) + resp, _ = sjson.SetRawBytes(resp, "output.-1", itemJSON) } if parts := root.Get("candidates.0.content.parts"); parts.Exists() && parts.IsArray() { @@ -696,15 +697,15 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string name := fc.Get("name").String() args := fc.Get("args") callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1)) - itemJSON := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` - itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID)) - itemJSON, _ = sjson.Set(itemJSON, "call_id", callID) - itemJSON, _ = sjson.Set(itemJSON, "name", name) + itemJSON := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) + itemJSON, _ = sjson.SetBytes(itemJSON, "id", fmt.Sprintf("fc_%s", callID)) + itemJSON, _ = sjson.SetBytes(itemJSON, "call_id", callID) + itemJSON, _ = sjson.SetBytes(itemJSON, "name", name) argsStr := "" if args.Exists() { argsStr = args.Raw } - itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr) + itemJSON, _ = sjson.SetBytes(itemJSON, "arguments", argsStr) appendOutput(itemJSON) return true } @@ -715,23 +716,23 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string // Reasoning output item if reasoningText.Len() > 0 || reasoningEncrypted != "" { rid := strings.TrimPrefix(id, "resp_") - itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}` - itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid)) - itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted) + itemJSON := []byte(`{"id":"","type":"reasoning","encrypted_content":""}`) + itemJSON, _ = sjson.SetBytes(itemJSON, "id", fmt.Sprintf("rs_%s", rid)) + itemJSON, _ = sjson.SetBytes(itemJSON, "encrypted_content", reasoningEncrypted) if reasoningText.Len() > 0 { - summaryJSON := `{"type":"summary_text","text":""}` - summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String()) - itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]") - itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON) + summaryJSON := []byte(`{"type":"summary_text","text":""}`) + summaryJSON, _ = sjson.SetBytes(summaryJSON, "text", reasoningText.String()) + itemJSON, _ = sjson.SetRawBytes(itemJSON, "summary", []byte(`[]`)) + itemJSON, _ = sjson.SetRawBytes(itemJSON, "summary.-1", summaryJSON) } appendOutput(itemJSON) } // Assistant message output item if haveMessage { - itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` - itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_"))) - itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String()) + itemJSON := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) + itemJSON, _ = sjson.SetBytes(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_"))) + itemJSON, _ = sjson.SetBytes(itemJSON, "content.0.text", messageText.String()) appendOutput(itemJSON) } @@ -739,18 +740,18 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string if um := root.Get("usageMetadata"); um.Exists() { // input tokens = prompt only (thoughts go to output) input := um.Get("promptTokenCount").Int() - resp, _ = sjson.Set(resp, "usage.input_tokens", input) + resp, _ = sjson.SetBytes(resp, "usage.input_tokens", input) // cached token details: align with OpenAI "cached_tokens" semantics. - resp, _ = sjson.Set(resp, "usage.input_tokens_details.cached_tokens", um.Get("cachedContentTokenCount").Int()) + resp, _ = sjson.SetBytes(resp, "usage.input_tokens_details.cached_tokens", um.Get("cachedContentTokenCount").Int()) // output tokens if v := um.Get("candidatesTokenCount"); v.Exists() { - resp, _ = sjson.Set(resp, "usage.output_tokens", v.Int()) + resp, _ = sjson.SetBytes(resp, "usage.output_tokens", v.Int()) } if v := um.Get("thoughtsTokenCount"); v.Exists() { - resp, _ = sjson.Set(resp, "usage.output_tokens_details.reasoning_tokens", v.Int()) + resp, _ = sjson.SetBytes(resp, "usage.output_tokens_details.reasoning_tokens", v.Int()) } if v := um.Get("totalTokenCount"); v.Exists() { - resp, _ = sjson.Set(resp, "usage.total_tokens", v.Int()) + resp, _ = sjson.SetBytes(resp, "usage.total_tokens", v.Int()) } } diff --git a/internal/translator/gemini/openai/responses/gemini_openai-responses_response_test.go b/internal/translator/gemini/openai/responses/gemini_openai-responses_response_test.go index 9899c594..715fdfd6 100644 --- a/internal/translator/gemini/openai/responses/gemini_openai-responses_response_test.go +++ b/internal/translator/gemini/openai/responses/gemini_openai-responses_response_test.go @@ -8,10 +8,10 @@ import ( "github.com/tidwall/gjson" ) -func parseSSEEvent(t *testing.T, chunk string) (string, gjson.Result) { +func parseSSEEvent(t *testing.T, chunk []byte) (string, gjson.Result) { t.Helper() - lines := strings.Split(chunk, "\n") + lines := strings.Split(string(chunk), "\n") if len(lines) < 2 { t.Fatalf("unexpected SSE chunk: %q", chunk) } @@ -39,7 +39,7 @@ func TestConvertGeminiResponseToOpenAIResponses_UnwrapAndAggregateText(t *testin originalReq := []byte(`{"instructions":"test instructions","model":"gpt-5","max_output_tokens":123}`) var param any - var out []string + var out [][]byte for _, line := range in { out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", originalReq, nil, []byte(line), ¶m)...) } @@ -163,7 +163,7 @@ func TestConvertGeminiResponseToOpenAIResponses_ReasoningEncryptedContent(t *tes } var param any - var out []string + var out [][]byte for _, line := range in { out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...) } @@ -203,7 +203,7 @@ func TestConvertGeminiResponseToOpenAIResponses_FunctionCallEventOrder(t *testin } var param any - var out []string + var out [][]byte for _, line := range in { out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...) } @@ -307,7 +307,7 @@ func TestConvertGeminiResponseToOpenAIResponses_ResponseOutputOrdering(t *testin } var param any - var out []string + var out [][]byte for _, line := range in { out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...) } diff --git a/internal/translator/openai/claude/openai_claude_request.go b/internal/translator/openai/claude/openai_claude_request.go index b5280af8..f12dd0c6 100644 --- a/internal/translator/openai/claude/openai_claude_request.go +++ b/internal/translator/openai/claude/openai_claude_request.go @@ -19,23 +19,23 @@ import ( func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := inputRawJSON // Base OpenAI Chat Completions API template - out := `{"model":"","messages":[]}` + out := []byte(`{"model":"","messages":[]}`) root := gjson.ParseBytes(rawJSON) // Model mapping - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Max tokens if maxTokens := root.Get("max_tokens"); maxTokens.Exists() { - out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } // Temperature if temp := root.Get("temperature"); temp.Exists() { - out, _ = sjson.Set(out, "temperature", temp.Float()) + out, _ = sjson.SetBytes(out, "temperature", temp.Float()) } else if topP := root.Get("top_p"); topP.Exists() { // Top P - out, _ = sjson.Set(out, "top_p", topP.Float()) + out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Stop sequences -> stop @@ -48,16 +48,16 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream }) if len(stops) > 0 { if len(stops) == 1 { - out, _ = sjson.Set(out, "stop", stops[0]) + out, _ = sjson.SetBytes(out, "stop", stops[0]) } else { - out, _ = sjson.Set(out, "stop", stops) + out, _ = sjson.SetBytes(out, "stop", stops) } } } } // Stream - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // Thinking: Convert Claude thinking.budget_tokens to OpenAI reasoning_effort if thinkingConfig := root.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() { @@ -67,12 +67,12 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() { budget := int(budgetTokens.Int()) if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } } else { // No budget_tokens specified, default to "auto" for enabled thinking if effort, ok := thinking.ConvertBudgetToLevel(-1); ok && effort != "" { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } } case "adaptive", "auto": @@ -83,30 +83,30 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream effort = strings.ToLower(strings.TrimSpace(v.String())) } if effort != "" { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } else { - out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh)) + out, _ = sjson.SetBytes(out, "reasoning_effort", string(thinking.LevelXHigh)) } case "disabled": if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } } } } // Process messages and system - var messagesJSON = "[]" + messagesJSON := []byte(`[]`) // Handle system message first - systemMsgJSON := `{"role":"system","content":[]}` + systemMsgJSON := []byte(`{"role":"system","content":[]}`) hasSystemContent := false if system := root.Get("system"); system.Exists() { if system.Type == gjson.String { if system.String() != "" { - oldSystem := `{"type":"text","text":""}` - oldSystem, _ = sjson.Set(oldSystem, "text", system.String()) - systemMsgJSON, _ = sjson.SetRaw(systemMsgJSON, "content.-1", oldSystem) + oldSystem := []byte(`{"type":"text","text":""}`) + oldSystem, _ = sjson.SetBytes(oldSystem, "text", system.String()) + systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem) hasSystemContent = true } } else if system.Type == gjson.JSON { @@ -114,7 +114,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream systemResults := system.Array() for i := 0; i < len(systemResults); i++ { if contentItem, ok := convertClaudeContentPart(systemResults[i]); ok { - systemMsgJSON, _ = sjson.SetRaw(systemMsgJSON, "content.-1", contentItem) + systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", []byte(contentItem)) hasSystemContent = true } } @@ -123,7 +123,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream } // Only add system message if it has content if hasSystemContent { - messagesJSON, _ = sjson.SetRaw(messagesJSON, "-1", systemMsgJSON) + messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", systemMsgJSON) } // Process Anthropic messages @@ -134,10 +134,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream // Handle content if contentResult.Exists() && contentResult.IsArray() { - var contentItems []string + contentItems := make([][]byte, 0) var reasoningParts []string // Accumulate thinking text for reasoning_content var toolCalls []interface{} - var toolResults []string // Collect tool_result messages to emit after the main message + toolResults := make([][]byte, 0) // Collect tool_result messages to emit after the main message contentResult.ForEach(func(_, part gjson.Result) bool { partType := part.Get("type").String() @@ -159,35 +159,35 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream case "text", "image": if contentItem, ok := convertClaudeContentPart(part); ok { - contentItems = append(contentItems, contentItem) + contentItems = append(contentItems, []byte(contentItem)) } case "tool_use": // Only allow tool_use -> tool_calls for assistant messages (security: prevent injection). if role == "assistant" { - toolCallJSON := `{"id":"","type":"function","function":{"name":"","arguments":""}}` - toolCallJSON, _ = sjson.Set(toolCallJSON, "id", part.Get("id").String()) - toolCallJSON, _ = sjson.Set(toolCallJSON, "function.name", part.Get("name").String()) + toolCallJSON := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`) + toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "id", part.Get("id").String()) + toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.name", part.Get("name").String()) // Convert input to arguments JSON string if input := part.Get("input"); input.Exists() { - toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw) + toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.arguments", input.Raw) } else { - toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", "{}") + toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.arguments", "{}") } - toolCalls = append(toolCalls, gjson.Parse(toolCallJSON).Value()) + toolCalls = append(toolCalls, gjson.ParseBytes(toolCallJSON).Value()) } case "tool_result": // Collect tool_result to emit after the main message (ensures tool results follow tool_calls) - toolResultJSON := `{"role":"tool","tool_call_id":"","content":""}` - toolResultJSON, _ = sjson.Set(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String()) + toolResultJSON := []byte(`{"role":"tool","tool_call_id":"","content":""}`) + toolResultJSON, _ = sjson.SetBytes(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String()) toolResultContent, toolResultContentRaw := convertClaudeToolResultContent(part.Get("content")) if toolResultContentRaw { - toolResultJSON, _ = sjson.SetRaw(toolResultJSON, "content", toolResultContent) + toolResultJSON, _ = sjson.SetRawBytes(toolResultJSON, "content", []byte(toolResultContent)) } else { - toolResultJSON, _ = sjson.Set(toolResultJSON, "content", toolResultContent) + toolResultJSON, _ = sjson.SetBytes(toolResultJSON, "content", toolResultContent) } toolResults = append(toolResults, toolResultJSON) } @@ -209,53 +209,53 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream // Therefore, we emit tool_result messages FIRST (they respond to the previous assistant's tool_calls), // then emit the current message's content. for _, toolResultJSON := range toolResults { - messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(toolResultJSON).Value()) + messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", toolResultJSON) } // For assistant messages: emit a single unified message with content, tool_calls, and reasoning_content // This avoids splitting into multiple assistant messages which breaks OpenAI tool-call adjacency if role == "assistant" { if hasContent || hasReasoning || hasToolCalls { - msgJSON := `{"role":"assistant"}` + msgJSON := []byte(`{"role":"assistant"}`) // Add content (as array if we have items, empty string if reasoning-only) if hasContent { - contentArrayJSON := "[]" + contentArrayJSON := []byte(`[]`) for _, contentItem := range contentItems { - contentArrayJSON, _ = sjson.SetRaw(contentArrayJSON, "-1", contentItem) + contentArrayJSON, _ = sjson.SetRawBytes(contentArrayJSON, "-1", contentItem) } - msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON) + msgJSON, _ = sjson.SetRawBytes(msgJSON, "content", contentArrayJSON) } else { // Ensure content field exists for OpenAI compatibility - msgJSON, _ = sjson.Set(msgJSON, "content", "") + msgJSON, _ = sjson.SetBytes(msgJSON, "content", "") } // Add reasoning_content if present if hasReasoning { - msgJSON, _ = sjson.Set(msgJSON, "reasoning_content", reasoningContent) + msgJSON, _ = sjson.SetBytes(msgJSON, "reasoning_content", reasoningContent) } // Add tool_calls if present (in same message as content) if hasToolCalls { - msgJSON, _ = sjson.Set(msgJSON, "tool_calls", toolCalls) + msgJSON, _ = sjson.SetBytes(msgJSON, "tool_calls", toolCalls) } - messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value()) + messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON) } } else { // For non-assistant roles: emit content message if we have content // If the message only contains tool_results (no text/image), we still processed them above if hasContent { - msgJSON := `{"role":""}` - msgJSON, _ = sjson.Set(msgJSON, "role", role) + msgJSON := []byte(`{"role":""}`) + msgJSON, _ = sjson.SetBytes(msgJSON, "role", role) - contentArrayJSON := "[]" + contentArrayJSON := []byte(`[]`) for _, contentItem := range contentItems { - contentArrayJSON, _ = sjson.SetRaw(contentArrayJSON, "-1", contentItem) + contentArrayJSON, _ = sjson.SetRawBytes(contentArrayJSON, "-1", contentItem) } - msgJSON, _ = sjson.SetRaw(msgJSON, "content", contentArrayJSON) + msgJSON, _ = sjson.SetRawBytes(msgJSON, "content", contentArrayJSON) - messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value()) + messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON) } else if hasToolResults && !hasContent { // tool_results already emitted above, no additional user message needed } @@ -263,10 +263,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream } else if contentResult.Exists() && contentResult.Type == gjson.String { // Simple string content - msgJSON := `{"role":"","content":""}` - msgJSON, _ = sjson.Set(msgJSON, "role", role) - msgJSON, _ = sjson.Set(msgJSON, "content", contentResult.String()) - messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value()) + msgJSON := []byte(`{"role":"","content":""}`) + msgJSON, _ = sjson.SetBytes(msgJSON, "role", role) + msgJSON, _ = sjson.SetBytes(msgJSON, "content", contentResult.String()) + messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON) } return true @@ -274,30 +274,30 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream } // Set messages - if gjson.Parse(messagesJSON).IsArray() && len(gjson.Parse(messagesJSON).Array()) > 0 { - out, _ = sjson.SetRaw(out, "messages", messagesJSON) + if msgs := gjson.ParseBytes(messagesJSON); msgs.IsArray() && len(msgs.Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "messages", messagesJSON) } // Process tools - convert Anthropic tools to OpenAI functions if tools := root.Get("tools"); tools.Exists() && tools.IsArray() { - var toolsJSON = "[]" + toolsJSON := []byte(`[]`) tools.ForEach(func(_, tool gjson.Result) bool { - openAIToolJSON := `{"type":"function","function":{"name":"","description":""}}` - openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.name", tool.Get("name").String()) - openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.description", tool.Get("description").String()) + openAIToolJSON := []byte(`{"type":"function","function":{"name":"","description":""}}`) + openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.name", tool.Get("name").String()) + openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.description", tool.Get("description").String()) // Convert Anthropic input_schema to OpenAI function parameters if inputSchema := tool.Get("input_schema"); inputSchema.Exists() { - openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.parameters", inputSchema.Value()) + openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.parameters", inputSchema.Value()) } - toolsJSON, _ = sjson.Set(toolsJSON, "-1", gjson.Parse(openAIToolJSON).Value()) + toolsJSON, _ = sjson.SetRawBytes(toolsJSON, "-1", openAIToolJSON) return true }) - if gjson.Parse(toolsJSON).IsArray() && len(gjson.Parse(toolsJSON).Array()) > 0 { - out, _ = sjson.SetRaw(out, "tools", toolsJSON) + if parsed := gjson.ParseBytes(toolsJSON); parsed.IsArray() && len(parsed.Array()) > 0 { + out, _ = sjson.SetRawBytes(out, "tools", toolsJSON) } } @@ -305,27 +305,27 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream if toolChoice := root.Get("tool_choice"); toolChoice.Exists() { switch toolChoice.Get("type").String() { case "auto": - out, _ = sjson.Set(out, "tool_choice", "auto") + out, _ = sjson.SetBytes(out, "tool_choice", "auto") case "any": - out, _ = sjson.Set(out, "tool_choice", "required") + out, _ = sjson.SetBytes(out, "tool_choice", "required") case "tool": // Specific tool choice toolName := toolChoice.Get("name").String() - toolChoiceJSON := `{"type":"function","function":{"name":""}}` - toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "function.name", toolName) - out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON) + toolChoiceJSON := []byte(`{"type":"function","function":{"name":""}}`) + toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "function.name", toolName) + out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON) default: // Default to auto if not specified - out, _ = sjson.Set(out, "tool_choice", "auto") + out, _ = sjson.SetBytes(out, "tool_choice", "auto") } } // Handle user parameter (for tracking) if user := root.Get("user"); user.Exists() { - out, _ = sjson.Set(out, "user", user.String()) + out, _ = sjson.SetBytes(out, "user", user.String()) } - return []byte(out) + return out } func convertClaudeContentPart(part gjson.Result) (string, bool) { @@ -337,9 +337,9 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) { if strings.TrimSpace(text) == "" { return "", false } - textContent := `{"type":"text","text":""}` - textContent, _ = sjson.Set(textContent, "text", text) - return textContent, true + textContent := []byte(`{"type":"text","text":""}`) + textContent, _ = sjson.SetBytes(textContent, "text", text) + return string(textContent), true case "image": var imageURL string @@ -369,10 +369,10 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) { return "", false } - imageContent := `{"type":"image_url","image_url":{"url":""}}` - imageContent, _ = sjson.Set(imageContent, "image_url.url", imageURL) + imageContent := []byte(`{"type":"image_url","image_url":{"url":""}}`) + imageContent, _ = sjson.SetBytes(imageContent, "image_url.url", imageURL) - return imageContent, true + return string(imageContent), true default: return "", false @@ -390,26 +390,26 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) { if content.IsArray() { var parts []string - contentJSON := "[]" + contentJSON := []byte(`[]`) hasImagePart := false content.ForEach(func(_, item gjson.Result) bool { switch { case item.Type == gjson.String: text := item.String() parts = append(parts, text) - textContent := `{"type":"text","text":""}` - textContent, _ = sjson.Set(textContent, "text", text) - contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent) + textContent := []byte(`{"type":"text","text":""}`) + textContent, _ = sjson.SetBytes(textContent, "text", text) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", textContent) case item.IsObject() && item.Get("type").String() == "text": text := item.Get("text").String() parts = append(parts, text) - textContent := `{"type":"text","text":""}` - textContent, _ = sjson.Set(textContent, "text", text) - contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent) + textContent := []byte(`{"type":"text","text":""}`) + textContent, _ = sjson.SetBytes(textContent, "text", text) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", textContent) case item.IsObject() && item.Get("type").String() == "image": contentItem, ok := convertClaudeContentPart(item) if ok { - contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", []byte(contentItem)) hasImagePart = true } else { parts = append(parts, item.Raw) @@ -423,7 +423,7 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) { }) if hasImagePart { - return contentJSON, true + return string(contentJSON), true } joined := strings.Join(parts, "\n\n") @@ -437,9 +437,9 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) { if content.Get("type").String() == "image" { contentItem, ok := convertClaudeContentPart(content) if ok { - contentJSON := "[]" - contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem) - return contentJSON, true + contentJSON := []byte(`[]`) + contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", []byte(contentItem)) + return string(contentJSON), true } } if text := content.Get("text"); text.Exists() && text.Type == gjson.String { diff --git a/internal/translator/openai/claude/openai_claude_response.go b/internal/translator/openai/claude/openai_claude_response.go index eddead62..46c75898 100644 --- a/internal/translator/openai/claude/openai_claude_response.go +++ b/internal/translator/openai/claude/openai_claude_response.go @@ -8,9 +8,9 @@ package claude import ( "bytes" "context" - "fmt" "strings" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/router-for-me/CLIProxyAPI/v6/internal/util" "github.com/tidwall/gjson" "github.com/tidwall/sjson" @@ -73,8 +73,8 @@ type ToolCallAccumulator struct { // - param: A pointer to a parameter object for the conversion. // // Returns: -// - []string: A slice of strings, each containing an Anthropic-compatible JSON response. -func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of byte chunks, each containing an Anthropic-compatible JSON response. +func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertOpenAIResponseToAnthropicParams{ MessageID: "", @@ -97,7 +97,7 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR } if !bytes.HasPrefix(rawJSON, dataTag) { - return []string{} + return [][]byte{} } rawJSON = bytes.TrimSpace(rawJSON[5:]) @@ -106,8 +106,7 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR } // Check if this is the [DONE] marker - rawStr := strings.TrimSpace(string(rawJSON)) - if rawStr == "[DONE]" { + if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) { return convertOpenAIDoneToAnthropic((*param).(*ConvertOpenAIResponseToAnthropicParams)) } @@ -130,9 +129,9 @@ func effectiveOpenAIFinishReason(param *ConvertOpenAIResponseToAnthropicParams) } // convertOpenAIStreamingChunkToAnthropic converts OpenAI streaming chunk to Anthropic streaming events -func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) []string { +func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAIResponseToAnthropicParams) [][]byte { root := gjson.ParseBytes(rawJSON) - var results []string + var results [][]byte // Initialize parameters if needed if param.MessageID == "" { @@ -150,10 +149,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI if delta := root.Get("choices.0.delta"); delta.Exists() { if !param.MessageStarted { // Send message_start event - messageStartJSON := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}` - messageStartJSON, _ = sjson.Set(messageStartJSON, "message.id", param.MessageID) - messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model) - results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n") + messageStartJSON := []byte(`{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`) + messageStartJSON, _ = sjson.SetBytes(messageStartJSON, "message.id", param.MessageID) + messageStartJSON, _ = sjson.SetBytes(messageStartJSON, "message.model", param.Model) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_start", messageStartJSON, 2)) param.MessageStarted = true // Don't send content_block_start for text here - wait for actual content @@ -172,15 +171,17 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI param.NextContentBlockIndex++ } contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}` - contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex) - results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n") + contentBlockStartJSONBytes := []byte(contentBlockStartJSON) + contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", param.ThinkingContentBlockIndex) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2)) param.ThinkingContentBlockStarted = true } thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}` - thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex) - thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText) - results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n") + thinkingDeltaJSONBytes := []byte(thinkingDeltaJSON) + thinkingDeltaJSONBytes, _ = sjson.SetBytes(thinkingDeltaJSONBytes, "index", param.ThinkingContentBlockIndex) + thinkingDeltaJSONBytes, _ = sjson.SetBytes(thinkingDeltaJSONBytes, "delta.thinking", reasoningText) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", thinkingDeltaJSONBytes, 2)) } } @@ -194,15 +195,17 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI param.NextContentBlockIndex++ } contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}` - contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex) - results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n") + contentBlockStartJSONBytes := []byte(contentBlockStartJSON) + contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", param.TextContentBlockIndex) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2)) param.TextContentBlockStarted = true } contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}` - contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex) - contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String()) - results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n") + contentDeltaJSONBytes := []byte(contentDeltaJSON) + contentDeltaJSONBytes, _ = sjson.SetBytes(contentDeltaJSONBytes, "index", param.TextContentBlockIndex) + contentDeltaJSONBytes, _ = sjson.SetBytes(contentDeltaJSONBytes, "delta.text", content.String()) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", contentDeltaJSONBytes, 2)) // Accumulate content param.ContentAccumulator.WriteString(content.String()) @@ -242,10 +245,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI // Send content_block_start for tool_use contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}` - contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex) - contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", util.SanitizeClaudeToolID(accumulator.ID)) - contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name) - results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n") + contentBlockStartJSONBytes := []byte(contentBlockStartJSON) + contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", blockIndex) + contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "content_block.id", util.SanitizeClaudeToolID(accumulator.ID)) + contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "content_block.name", accumulator.Name) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2)) } // Handle function arguments @@ -273,9 +277,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI // Send content_block_stop for thinking content if needed if param.ThinkingContentBlockStarted { - contentBlockStopJSON := `{"type":"content_block_stop","index":0}` - contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) - results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") + contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`) + contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2)) param.ThinkingContentBlockStarted = false param.ThinkingContentBlockIndex = -1 } @@ -291,15 +295,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI // Send complete input_json_delta with all accumulated arguments if accumulator.Arguments.Len() > 0 { - inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex) - inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String())) - results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n") + inputDeltaJSON := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`) + inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "index", blockIndex) + inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String())) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", inputDeltaJSON, 2)) } - contentBlockStopJSON := `{"type":"content_block_stop","index":0}` - contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex) - results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") + contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`) + contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", blockIndex) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2)) delete(param.ToolCallBlockIndexes, index) } param.ContentBlocksStopped = true @@ -316,14 +320,14 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI if usage.Exists() && usage.Type != gjson.Null { inputTokens, outputTokens, cachedTokens = extractOpenAIUsage(usage) // Send message_delta with usage - messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` - messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param))) - messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens) - messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens) + messageDeltaJSON := []byte(`{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) + messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param))) + messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.input_tokens", inputTokens) + messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.output_tokens", outputTokens) if cachedTokens > 0 { - messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.cache_read_input_tokens", cachedTokens) + messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.cache_read_input_tokens", cachedTokens) } - results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n") + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_delta", messageDeltaJSON, 2)) param.MessageDeltaSent = true emitMessageStopIfNeeded(param, &results) @@ -334,14 +338,14 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI } // convertOpenAIDoneToAnthropic handles the [DONE] marker and sends final events -func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) []string { - var results []string +func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) [][]byte { + var results [][]byte // Ensure all content blocks are stopped before final events if param.ThinkingContentBlockStarted { - contentBlockStopJSON := `{"type":"content_block_stop","index":0}` - contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) - results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") + contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`) + contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2)) param.ThinkingContentBlockStarted = false param.ThinkingContentBlockIndex = -1 } @@ -354,15 +358,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) blockIndex := param.toolContentBlockIndex(index) if accumulator.Arguments.Len() > 0 { - inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}` - inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex) - inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String())) - results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n") + inputDeltaJSON := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`) + inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "index", blockIndex) + inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String())) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", inputDeltaJSON, 2)) } - contentBlockStopJSON := `{"type":"content_block_stop","index":0}` - contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex) - results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") + contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`) + contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", blockIndex) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2)) delete(param.ToolCallBlockIndexes, index) } param.ContentBlocksStopped = true @@ -370,9 +374,9 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) // If we haven't sent message_delta yet (no usage info was received), send it now if param.FinishReason != "" && !param.MessageDeltaSent { - messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}` - messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param))) - results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n") + messageDeltaJSON := []byte(`{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`) + messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param))) + results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_delta", messageDeltaJSON, 2)) param.MessageDeltaSent = true } @@ -382,12 +386,12 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) } // convertOpenAINonStreamingToAnthropic converts OpenAI non-streaming response to Anthropic format -func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string { +func convertOpenAINonStreamingToAnthropic(rawJSON []byte) [][]byte { root := gjson.ParseBytes(rawJSON) - out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` - out, _ = sjson.Set(out, "id", root.Get("id").String()) - out, _ = sjson.Set(out, "model", root.Get("model").String()) + out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) + out, _ = sjson.SetBytes(out, "id", root.Get("id").String()) + out, _ = sjson.SetBytes(out, "model", root.Get("model").String()) // Process message content and tool calls if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 { @@ -398,59 +402,59 @@ func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string { if reasoningText == "" { continue } - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", reasoningText) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", reasoningText) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } // Handle text content if content := choice.Get("message.content"); content.Exists() && content.String() != "" { - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", content.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", content.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } // Handle tool calls if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() { toolCalls.ForEach(func(_, toolCall gjson.Result) bool { - toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}` - toolUseBlock, _ = sjson.Set(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String())) - toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String()) + toolUseBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String())) + toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "name", toolCall.Get("function.name").String()) argsStr := util.FixJSON(toolCall.Get("function.arguments").String()) if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { - toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw) + toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(argsJSON.Raw)) } else { - toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}") + toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`)) } } else { - toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}") + toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`)) } - out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock) + out, _ = sjson.SetRawBytes(out, "content.-1", toolUseBlock) return true }) } // Set stop reason if finishReason := choice.Get("finish_reason"); finishReason.Exists() { - out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String())) + out, _ = sjson.SetBytes(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String())) } } // Set usage information if usage := root.Get("usage"); usage.Exists() { inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(usage) - out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) - out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens) if cachedTokens > 0 { - out, _ = sjson.Set(out, "usage.cache_read_input_tokens", cachedTokens) + out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens) } } - return []string{out} + return [][]byte{out} } // mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents @@ -513,32 +517,32 @@ func collectOpenAIReasoningTexts(node gjson.Result) []string { return texts } -func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) { +func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) { if !param.ThinkingContentBlockStarted { return } - contentBlockStopJSON := `{"type":"content_block_stop","index":0}` - contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) - *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") + contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`) + contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex) + *results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2)) param.ThinkingContentBlockStarted = false param.ThinkingContentBlockIndex = -1 } -func emitMessageStopIfNeeded(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) { +func emitMessageStopIfNeeded(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) { if param.MessageStopSent { return } - *results = append(*results, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n") + *results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "message_stop", []byte(`{"type":"message_stop"}`), 2)) param.MessageStopSent = true } -func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) { +func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) { if !param.TextContentBlockStarted { return } - contentBlockStopJSON := `{"type":"content_block_stop","index":0}` - contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex) - *results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n") + contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`) + contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.TextContentBlockIndex) + *results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2)) param.TextContentBlockStarted = false param.TextContentBlockIndex = -1 } @@ -552,15 +556,15 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results // - param: A pointer to a parameter object for the conversion. // // Returns: -// - string: An Anthropic-compatible JSON response. -func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: An Anthropic-compatible JSON response. +func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { _ = requestRawJSON root := gjson.ParseBytes(rawJSON) toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON) - out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}` - out, _ = sjson.Set(out, "id", root.Get("id").String()) - out, _ = sjson.Set(out, "model", root.Get("model").String()) + out := []byte(`{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`) + out, _ = sjson.SetBytes(out, "id", root.Get("id").String()) + out, _ = sjson.SetBytes(out, "model", root.Get("model").String()) hasToolCall := false stopReasonSet := false @@ -569,7 +573,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina choice := choices.Array()[0] if finishReason := choice.Get("finish_reason"); finishReason.Exists() { - out, _ = sjson.Set(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String())) + out, _ = sjson.SetBytes(out, "stop_reason", mapOpenAIFinishReasonToAnthropic(finishReason.String())) stopReasonSet = true } @@ -583,9 +587,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina if textBuilder.Len() == 0 { return } - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", textBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", textBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) textBuilder.Reset() } @@ -593,9 +597,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina if thinkingBuilder.Len() == 0 { return } - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", thinkingBuilder.String()) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String()) + out, _ = sjson.SetRawBytes(out, "content.-1", block) thinkingBuilder.Reset() } @@ -611,23 +615,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina if toolCalls.IsArray() { toolCalls.ForEach(func(_, tc gjson.Result) bool { hasToolCall = true - toolUse := `{"type":"tool_use","id":"","name":"","input":{}}` - toolUse, _ = sjson.Set(toolUse, "id", util.SanitizeClaudeToolID(tc.Get("id").String())) - toolUse, _ = sjson.Set(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String())) + toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolUse, _ = sjson.SetBytes(toolUse, "id", util.SanitizeClaudeToolID(tc.Get("id").String())) + toolUse, _ = sjson.SetBytes(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String())) argsStr := util.FixJSON(tc.Get("function.arguments").String()) if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { - toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw) + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw)) } else { - toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(`{}`)) } } else { - toolUse, _ = sjson.SetRaw(toolUse, "input", "{}") + toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(`{}`)) } - out, _ = sjson.SetRaw(out, "content.-1", toolUse) + out, _ = sjson.SetRawBytes(out, "content.-1", toolUse) return true }) } @@ -647,9 +651,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina } else if contentResult.Type == gjson.String { textContent := contentResult.String() if textContent != "" { - block := `{"type":"text","text":""}` - block, _ = sjson.Set(block, "text", textContent) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"text","text":""}`) + block, _ = sjson.SetBytes(block, "text", textContent) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } } } @@ -659,32 +663,32 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina if reasoningText == "" { continue } - block := `{"type":"thinking","thinking":""}` - block, _ = sjson.Set(block, "thinking", reasoningText) - out, _ = sjson.SetRaw(out, "content.-1", block) + block := []byte(`{"type":"thinking","thinking":""}`) + block, _ = sjson.SetBytes(block, "thinking", reasoningText) + out, _ = sjson.SetRawBytes(out, "content.-1", block) } } if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() { toolCalls.ForEach(func(_, toolCall gjson.Result) bool { hasToolCall = true - toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}` - toolUseBlock, _ = sjson.Set(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String())) - toolUseBlock, _ = sjson.Set(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String())) + toolUseBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`) + toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String())) + toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String())) argsStr := util.FixJSON(toolCall.Get("function.arguments").String()) if argsStr != "" && gjson.Valid(argsStr) { argsJSON := gjson.Parse(argsStr) if argsJSON.IsObject() { - toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw) + toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(argsJSON.Raw)) } else { - toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}") + toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`)) } } else { - toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}") + toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`)) } - out, _ = sjson.SetRaw(out, "content.-1", toolUseBlock) + out, _ = sjson.SetRawBytes(out, "content.-1", toolUseBlock) return true }) } @@ -693,26 +697,26 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina if respUsage := root.Get("usage"); respUsage.Exists() { inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(respUsage) - out, _ = sjson.Set(out, "usage.input_tokens", inputTokens) - out, _ = sjson.Set(out, "usage.output_tokens", outputTokens) + out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens) + out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens) if cachedTokens > 0 { - out, _ = sjson.Set(out, "usage.cache_read_input_tokens", cachedTokens) + out, _ = sjson.SetBytes(out, "usage.cache_read_input_tokens", cachedTokens) } } if !stopReasonSet { if hasToolCall { - out, _ = sjson.Set(out, "stop_reason", "tool_use") + out, _ = sjson.SetBytes(out, "stop_reason", "tool_use") } else { - out, _ = sjson.Set(out, "stop_reason", "end_turn") + out, _ = sjson.SetBytes(out, "stop_reason", "end_turn") } } return out } -func ClaudeTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"input_tokens":%d}`, count) +func ClaudeTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.ClaudeInputTokensJSON(count) } func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64) { diff --git a/internal/translator/openai/gemini-cli/openai_gemini_response.go b/internal/translator/openai/gemini-cli/openai_gemini_response.go index b5977964..a7369dbf 100644 --- a/internal/translator/openai/gemini-cli/openai_gemini_response.go +++ b/internal/translator/openai/gemini-cli/openai_gemini_response.go @@ -7,10 +7,9 @@ package geminiCLI import ( "context" - "fmt" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" . "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/openai/gemini" - "github.com/tidwall/sjson" ) // ConvertOpenAIResponseToGeminiCLI converts OpenAI Chat Completions streaming response format to Gemini API format. @@ -24,14 +23,12 @@ import ( // - param: A pointer to a parameter object for the conversion. // // Returns: -// - []string: A slice of strings, each containing a Gemini-compatible JSON response. -func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Gemini-compatible JSON responses. +func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { outputs := ConvertOpenAIResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) - newOutputs := make([]string, 0) + newOutputs := make([][]byte, 0, len(outputs)) for i := 0; i < len(outputs); i++ { - json := `{"response": {}}` - output, _ := sjson.SetRaw(json, "response", outputs[i]) - newOutputs = append(newOutputs, output) + newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i])) } return newOutputs } @@ -45,14 +42,12 @@ func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, ori // - param: A pointer to a parameter object for the conversion. // // Returns: -// - string: A Gemini-compatible JSON response. -func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { - strJSON := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) - json := `{"response": {}}` - strJSON, _ = sjson.SetRaw(json, "response", strJSON) - return strJSON +// - []byte: A Gemini-compatible JSON response. +func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { + out := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) + return translatorcommon.WrapGeminiCLIResponse(out) } -func GeminiCLITokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiCLITokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } diff --git a/internal/translator/openai/gemini/openai_gemini_request.go b/internal/translator/openai/gemini/openai_gemini_request.go index 167b71e9..b4edbb1d 100644 --- a/internal/translator/openai/gemini/openai_gemini_request.go +++ b/internal/translator/openai/gemini/openai_gemini_request.go @@ -22,7 +22,7 @@ import ( func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := inputRawJSON // Base OpenAI Chat Completions API template - out := `{"model":"","messages":[]}` + out := []byte(`{"model":"","messages":[]}`) root := gjson.ParseBytes(rawJSON) @@ -39,29 +39,29 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream } // Model mapping - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Generation config mapping if genConfig := root.Get("generationConfig"); genConfig.Exists() { // Temperature if temp := genConfig.Get("temperature"); temp.Exists() { - out, _ = sjson.Set(out, "temperature", temp.Float()) + out, _ = sjson.SetBytes(out, "temperature", temp.Float()) } // Max tokens if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() { - out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } // Top P if topP := genConfig.Get("topP"); topP.Exists() { - out, _ = sjson.Set(out, "top_p", topP.Float()) + out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } // Top K (OpenAI doesn't have direct equivalent, but we can map it) if topK := genConfig.Get("topK"); topK.Exists() { // Store as custom parameter for potential use - out, _ = sjson.Set(out, "top_k", topK.Int()) + out, _ = sjson.SetBytes(out, "top_k", topK.Int()) } // Stop sequences @@ -72,13 +72,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream return true }) if len(stops) > 0 { - out, _ = sjson.Set(out, "stop", stops) + out, _ = sjson.SetBytes(out, "stop", stops) } } // Candidate count (OpenAI 'n' parameter) if candidateCount := genConfig.Get("candidateCount"); candidateCount.Exists() { - out, _ = sjson.Set(out, "n", candidateCount.Int()) + out, _ = sjson.SetBytes(out, "n", candidateCount.Int()) } // Map Gemini thinkingConfig to OpenAI reasoning_effort. @@ -92,7 +92,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream if thinkingLevel.Exists() { effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String())) if effort != "" { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } } else { thinkingBudget := thinkingConfig.Get("thinkingBudget") @@ -101,7 +101,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream } if thinkingBudget.Exists() { if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } } } @@ -109,7 +109,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream } // Stream parameter - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // Process contents (Gemini messages) -> OpenAI messages var toolCallIDs []string // Track tool call IDs for matching with tool results @@ -122,16 +122,16 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream } if systemInstruction.Exists() { parts := systemInstruction.Get("parts") - msg := `{"role":"system","content":[]}` + msg := []byte(`{"role":"system","content":[]}`) hasContent := false if parts.Exists() && parts.IsArray() { parts.ForEach(func(_, part gjson.Result) bool { // Handle text parts if text := part.Get("text"); text.Exists() { - contentPart := `{"type":"text","text":""}` - contentPart, _ = sjson.Set(contentPart, "text", text.String()) - msg, _ = sjson.SetRaw(msg, "content.-1", contentPart) + contentPart := []byte(`{"type":"text","text":""}`) + contentPart, _ = sjson.SetBytes(contentPart, "text", text.String()) + msg, _ = sjson.SetRawBytes(msg, "content.-1", contentPart) hasContent = true } @@ -144,9 +144,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream data := inlineData.Get("data").String() imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) - contentPart := `{"type":"image_url","image_url":{"url":""}}` - contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL) - msg, _ = sjson.SetRaw(msg, "content.-1", contentPart) + contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`) + contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL) + msg, _ = sjson.SetRawBytes(msg, "content.-1", contentPart) hasContent = true } return true @@ -154,7 +154,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream } if hasContent { - out, _ = sjson.SetRaw(out, "messages.-1", msg) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } } @@ -168,14 +168,14 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream role = "assistant" } - msg := `{"role":"","content":""}` - msg, _ = sjson.Set(msg, "role", role) + msg := []byte(`{"role":"","content":""}`) + msg, _ = sjson.SetBytes(msg, "role", role) var textBuilder strings.Builder - contentWrapper := `{"arr":[]}` + contentWrapper := []byte(`{"arr":[]}`) contentPartsCount := 0 onlyTextContent := true - toolCallsWrapper := `{"arr":[]}` + toolCallsWrapper := []byte(`{"arr":[]}`) toolCallsCount := 0 if parts.Exists() && parts.IsArray() { @@ -184,9 +184,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream if text := part.Get("text"); text.Exists() { formattedText := text.String() textBuilder.WriteString(formattedText) - contentPart := `{"type":"text","text":""}` - contentPart, _ = sjson.Set(contentPart, "text", formattedText) - contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart) + contentPart := []byte(`{"type":"text","text":""}`) + contentPart, _ = sjson.SetBytes(contentPart, "text", formattedText) + contentWrapper, _ = sjson.SetRawBytes(contentWrapper, "arr.-1", contentPart) contentPartsCount++ } @@ -201,9 +201,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream data := inlineData.Get("data").String() imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data) - contentPart := `{"type":"image_url","image_url":{"url":""}}` - contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL) - contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart) + contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`) + contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL) + contentWrapper, _ = sjson.SetRawBytes(contentWrapper, "arr.-1", contentPart) contentPartsCount++ } @@ -212,32 +212,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream toolCallID := genToolCallID() toolCallIDs = append(toolCallIDs, toolCallID) - toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}` - toolCall, _ = sjson.Set(toolCall, "id", toolCallID) - toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String()) + toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`) + toolCall, _ = sjson.SetBytes(toolCall, "id", toolCallID) + toolCall, _ = sjson.SetBytes(toolCall, "function.name", functionCall.Get("name").String()) // Convert args to arguments JSON string if args := functionCall.Get("args"); args.Exists() { - toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw) + toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", args.Raw) } else { - toolCall, _ = sjson.Set(toolCall, "function.arguments", "{}") + toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", "{}") } - toolCallsWrapper, _ = sjson.SetRaw(toolCallsWrapper, "arr.-1", toolCall) + toolCallsWrapper, _ = sjson.SetRawBytes(toolCallsWrapper, "arr.-1", toolCall) toolCallsCount++ } // Handle function responses (Gemini) -> tool role messages (OpenAI) if functionResponse := part.Get("functionResponse"); functionResponse.Exists() { // Create tool message for function response - toolMsg := `{"role":"tool","tool_call_id":"","content":""}` + toolMsg := []byte(`{"role":"tool","tool_call_id":"","content":""}`) // Convert response.content to JSON string if response := functionResponse.Get("response"); response.Exists() { if contentField := response.Get("content"); contentField.Exists() { - toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw) + toolMsg, _ = sjson.SetBytes(toolMsg, "content", contentField.Raw) } else { - toolMsg, _ = sjson.Set(toolMsg, "content", response.Raw) + toolMsg, _ = sjson.SetBytes(toolMsg, "content", response.Raw) } } @@ -246,13 +246,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream if len(toolCallIDs) > 0 { // Use the last tool call ID (simple matching by function name) // In a real implementation, you might want more sophisticated matching - toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1]) + toolMsg, _ = sjson.SetBytes(toolMsg, "tool_call_id", toolCallIDs[len(toolCallIDs)-1]) } else { // Generate a tool call ID if none available - toolMsg, _ = sjson.Set(toolMsg, "tool_call_id", genToolCallID()) + toolMsg, _ = sjson.SetBytes(toolMsg, "tool_call_id", genToolCallID()) } - out, _ = sjson.SetRaw(out, "messages.-1", toolMsg) + out, _ = sjson.SetRawBytes(out, "messages.-1", toolMsg) } return true @@ -262,18 +262,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream // Set content if contentPartsCount > 0 { if onlyTextContent { - msg, _ = sjson.Set(msg, "content", textBuilder.String()) + msg, _ = sjson.SetBytes(msg, "content", textBuilder.String()) } else { - msg, _ = sjson.SetRaw(msg, "content", gjson.Get(contentWrapper, "arr").Raw) + msg, _ = sjson.SetRawBytes(msg, "content", []byte(gjson.GetBytes(contentWrapper, "arr").Raw)) } } // Set tool calls if any if toolCallsCount > 0 { - msg, _ = sjson.SetRaw(msg, "tool_calls", gjson.Get(toolCallsWrapper, "arr").Raw) + msg, _ = sjson.SetRawBytes(msg, "tool_calls", []byte(gjson.GetBytes(toolCallsWrapper, "arr").Raw)) } - out, _ = sjson.SetRaw(out, "messages.-1", msg) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) return true }) } @@ -283,18 +283,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream tools.ForEach(func(_, tool gjson.Result) bool { if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() { functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool { - openAITool := `{"type":"function","function":{"name":"","description":""}}` - openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String()) - openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String()) + openAITool := []byte(`{"type":"function","function":{"name":"","description":""}}`) + openAITool, _ = sjson.SetBytes(openAITool, "function.name", funcDecl.Get("name").String()) + openAITool, _ = sjson.SetBytes(openAITool, "function.description", funcDecl.Get("description").String()) // Convert parameters schema if parameters := funcDecl.Get("parameters"); parameters.Exists() { - openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw) + openAITool, _ = sjson.SetRawBytes(openAITool, "function.parameters", []byte(parameters.Raw)) } else if parameters := funcDecl.Get("parametersJsonSchema"); parameters.Exists() { - openAITool, _ = sjson.SetRaw(openAITool, "function.parameters", parameters.Raw) + openAITool, _ = sjson.SetRawBytes(openAITool, "function.parameters", []byte(parameters.Raw)) } - out, _ = sjson.SetRaw(out, "tools.-1", openAITool) + out, _ = sjson.SetRawBytes(out, "tools.-1", openAITool) return true }) } @@ -308,14 +308,14 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream mode := functionCallingConfig.Get("mode").String() switch mode { case "NONE": - out, _ = sjson.Set(out, "tool_choice", "none") + out, _ = sjson.SetBytes(out, "tool_choice", "none") case "AUTO": - out, _ = sjson.Set(out, "tool_choice", "auto") + out, _ = sjson.SetBytes(out, "tool_choice", "auto") case "ANY": - out, _ = sjson.Set(out, "tool_choice", "required") + out, _ = sjson.SetBytes(out, "tool_choice", "required") } } } - return []byte(out) + return out } diff --git a/internal/translator/openai/gemini/openai_gemini_response.go b/internal/translator/openai/gemini/openai_gemini_response.go index 040f805c..092a778e 100644 --- a/internal/translator/openai/gemini/openai_gemini_response.go +++ b/internal/translator/openai/gemini/openai_gemini_response.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -44,8 +45,8 @@ type ToolCallAccumulator struct { // - param: A pointer to a parameter object for the conversion. // // Returns: -// - []string: A slice of strings, each containing a Gemini-compatible JSON response. -func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of Gemini-compatible JSON responses. +func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &ConvertOpenAIResponseToGeminiParams{ ToolCallsAccumulator: nil, @@ -55,8 +56,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR } // Handle [DONE] marker - if strings.TrimSpace(string(rawJSON)) == "[DONE]" { - return []string{} + if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) { + return [][]byte{} } if bytes.HasPrefix(rawJSON, []byte("data:")) { @@ -76,51 +77,51 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR if len(choices.Array()) == 0 { // This is a usage-only chunk, handle usage and return if usage := root.Get("usage"); usage.Exists() { - template := `{"candidates":[],"usageMetadata":{}}` + template := []byte(`{"candidates":[],"usageMetadata":{}}`) // Set model if available if model := root.Get("model"); model.Exists() { - template, _ = sjson.Set(template, "model", model.String()) + template, _ = sjson.SetBytes(template, "model", model.String()) } - template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) - template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) - template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 { - template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens) } - return []string{template} + return [][]byte{template} } - return []string{} + return [][]byte{} } - var results []string + var results [][]byte choices.ForEach(func(choiceIndex, choice gjson.Result) bool { // Base Gemini response template without finishReason; set when known - template := `{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}` + template := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`) // Set model if available if model := root.Get("model"); model.Exists() { - template, _ = sjson.Set(template, "model", model.String()) + template, _ = sjson.SetBytes(template, "model", model.String()) } _ = int(choice.Get("index").Int()) // choiceIdx not used in streaming delta := choice.Get("delta") - baseTemplate := template + baseTemplate := append([]byte(nil), template...) // Handle role (only in first chunk) if role := delta.Get("role"); role.Exists() && (*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk { // OpenAI assistant -> Gemini model if role.String() == "assistant" { - template, _ = sjson.Set(template, "candidates.0.content.role", "model") + template, _ = sjson.SetBytes(template, "candidates.0.content.role", "model") } (*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk = false results = append(results, template) return true } - var chunkOutputs []string + var chunkOutputs [][]byte // Handle reasoning/thinking delta if reasoning := delta.Get("reasoning_content"); reasoning.Exists() { @@ -128,9 +129,9 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR if reasoningText == "" { continue } - reasoningTemplate := baseTemplate - reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true) - reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText) + reasoningTemplate := append([]byte(nil), baseTemplate...) + reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.thought", true) + reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText) chunkOutputs = append(chunkOutputs, reasoningTemplate) } } @@ -141,8 +142,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR (*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText) // Create text part for this delta - contentTemplate := baseTemplate - contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText) + contentTemplate := append([]byte(nil), baseTemplate...) + contentTemplate, _ = sjson.SetBytes(contentTemplate, "candidates.0.content.parts.0.text", contentText) chunkOutputs = append(chunkOutputs, contentTemplate) } @@ -207,7 +208,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR // Handle finish reason if finishReason := choice.Get("finish_reason"); finishReason.Exists() { geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String()) - template, _ = sjson.Set(template, "candidates.0.finishReason", geminiFinishReason) + template, _ = sjson.SetBytes(template, "candidates.0.finishReason", geminiFinishReason) // If we have accumulated tool calls, output them now if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 { @@ -215,8 +216,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator { namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex) argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex) - template, _ = sjson.Set(template, namePath, accumulator.Name) - template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String())) + template, _ = sjson.SetBytes(template, namePath, accumulator.Name) + template, _ = sjson.SetRawBytes(template, argsPath, []byte(parseArgsToObjectRaw(accumulator.Arguments.String()))) partIndex++ } @@ -230,11 +231,11 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR // Handle usage information if usage := root.Get("usage"); usage.Exists() { - template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) - template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) - template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) + template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 { - template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens) + template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens) } results = append(results, template) return true @@ -244,7 +245,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR }) return results } - return []string{} + return [][]byte{} } // mapOpenAIFinishReasonToGemini maps OpenAI finish reasons to Gemini finish reasons @@ -310,7 +311,7 @@ func tolerantParseJSONObjectRaw(s string) string { runes := []rune(content) n := len(runes) i := 0 - result := "{}" + result := []byte(`{}`) for i < n { // Skip whitespace and commas @@ -362,10 +363,10 @@ func tolerantParseJSONObjectRaw(s string) string { valToken, ni := parseJSONStringRunes(runes, i) if ni == -1 { // Malformed; treat as empty string - result, _ = sjson.Set(result, sjsonKey, "") + result, _ = sjson.SetBytes(result, sjsonKey, "") i = n } else { - result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken)) + result, _ = sjson.SetBytes(result, sjsonKey, jsonStringTokenToRawString(valToken)) i = ni } case '{', '[': @@ -375,9 +376,9 @@ func tolerantParseJSONObjectRaw(s string) string { i = n } else { if gjson.Valid(seg) { - result, _ = sjson.SetRaw(result, sjsonKey, seg) + result, _ = sjson.SetRawBytes(result, sjsonKey, []byte(seg)) } else { - result, _ = sjson.Set(result, sjsonKey, seg) + result, _ = sjson.SetBytes(result, sjsonKey, seg) } i = ni } @@ -390,15 +391,15 @@ func tolerantParseJSONObjectRaw(s string) string { token := strings.TrimSpace(string(runes[i:j])) // Interpret common JSON atoms and numbers; otherwise treat as string if token == "true" { - result, _ = sjson.Set(result, sjsonKey, true) + result, _ = sjson.SetBytes(result, sjsonKey, true) } else if token == "false" { - result, _ = sjson.Set(result, sjsonKey, false) + result, _ = sjson.SetBytes(result, sjsonKey, false) } else if token == "null" { - result, _ = sjson.Set(result, sjsonKey, nil) + result, _ = sjson.SetBytes(result, sjsonKey, nil) } else if numVal, ok := tryParseNumber(token); ok { - result, _ = sjson.Set(result, sjsonKey, numVal) + result, _ = sjson.SetBytes(result, sjsonKey, numVal) } else { - result, _ = sjson.Set(result, sjsonKey, token) + result, _ = sjson.SetBytes(result, sjsonKey, token) } i = j } @@ -412,7 +413,7 @@ func tolerantParseJSONObjectRaw(s string) string { } } - return result + return string(result) } // parseJSONStringRunes returns the JSON string token (including quotes) and the index just after it. @@ -531,16 +532,16 @@ func tryParseNumber(s string) (interface{}, bool) { // - param: A pointer to a parameter object for the conversion. // // Returns: -// - string: A Gemini-compatible JSON response. -func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +// - []byte: A Gemini-compatible JSON response. +func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { root := gjson.ParseBytes(rawJSON) // Base Gemini response template without finishReason; set when known - out := `{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}` + out := []byte(`{"candidates":[{"content":{"parts":[],"role":"model"},"index":0}]}`) // Set model if available if model := root.Get("model"); model.Exists() { - out, _ = sjson.Set(out, "model", model.String()) + out, _ = sjson.SetBytes(out, "model", model.String()) } // Process choices @@ -552,7 +553,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina // Set role if role := message.Get("role"); role.Exists() { if role.String() == "assistant" { - out, _ = sjson.Set(out, "candidates.0.content.role", "model") + out, _ = sjson.SetBytes(out, "candidates.0.content.role", "model") } } @@ -564,15 +565,15 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina if reasoningText == "" { continue } - out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true) - out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText) + out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true) + out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), reasoningText) partIndex++ } } // Handle content first if content := message.Get("content"); content.Exists() && content.String() != "" { - out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String()) + out, _ = sjson.SetBytes(out, fmt.Sprintf("candidates.0.content.parts.%d.text", partIndex), content.String()) partIndex++ } @@ -586,8 +587,8 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex) argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex) - out, _ = sjson.Set(out, namePath, functionName) - out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs)) + out, _ = sjson.SetBytes(out, namePath, functionName) + out, _ = sjson.SetRawBytes(out, argsPath, []byte(parseArgsToObjectRaw(functionArgs))) partIndex++ } return true @@ -597,11 +598,11 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina // Handle finish reason if finishReason := choice.Get("finish_reason"); finishReason.Exists() { geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String()) - out, _ = sjson.Set(out, "candidates.0.finishReason", geminiFinishReason) + out, _ = sjson.SetBytes(out, "candidates.0.finishReason", geminiFinishReason) } // Set index - out, _ = sjson.Set(out, "candidates.0.index", choiceIdx) + out, _ = sjson.SetBytes(out, "candidates.0.index", choiceIdx) return true }) @@ -609,19 +610,19 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina // Handle usage information if usage := root.Get("usage"); usage.Exists() { - out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) - out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) - out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) + out, _ = sjson.SetBytes(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int()) + out, _ = sjson.SetBytes(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int()) + out, _ = sjson.SetBytes(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int()) if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 { - out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens) + out, _ = sjson.SetBytes(out, "usageMetadata.thoughtsTokenCount", reasoningTokens) } } return out } -func GeminiTokenCount(ctx context.Context, count int64) string { - return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count) +func GeminiTokenCount(ctx context.Context, count int64) []byte { + return translatorcommon.GeminiTokenCountJSON(count) } func reasoningTokensFromUsage(usage gjson.Result) int64 { diff --git a/internal/translator/openai/openai/chat-completions/openai_openai_response.go b/internal/translator/openai/openai/chat-completions/openai_openai_response.go index ff2acc52..9320a3de 100644 --- a/internal/translator/openai/openai/chat-completions/openai_openai_response.go +++ b/internal/translator/openai/openai/chat-completions/openai_openai_response.go @@ -1,8 +1,5 @@ -// Package openai provides response translation functionality for Gemini CLI to OpenAI API compatibility. -// This package handles the conversion of Gemini CLI API responses into OpenAI Chat Completions-compatible -// JSON format, transforming streaming events and non-streaming responses into the format -// expected by OpenAI API clients. It supports both streaming and non-streaming modes, -// handling text content, tool calls, reasoning content, and usage metadata appropriately. +// Package chat_completions provides passthrough response translation for OpenAI Chat Completions. +// It normalizes OpenAI-compatible SSE lines by stripping the "data:" prefix and dropping "[DONE]". package chat_completions import ( @@ -10,11 +7,9 @@ import ( "context" ) -// ConvertOpenAIResponseToOpenAI translates a single chunk of a streaming response from the -// Gemini CLI API format to the OpenAI Chat Completions streaming format. -// It processes various Gemini CLI event types and transforms them into OpenAI-compatible JSON responses. -// The function handles text content, tool calls, reasoning content, and usage metadata, outputting -// responses that match the OpenAI API format. It supports incremental updates for streaming responses. +// ConvertOpenAIResponseToOpenAI normalizes a single chunk of an OpenAI-compatible streaming response. +// If the chunk is an SSE "data:" line, the prefix is stripped and the remaining JSON payload is returned. +// The "[DONE]" marker yields no output. // // Parameters: // - ctx: The context for the request, used for cancellation and timeout handling @@ -23,21 +18,18 @@ import ( // - param: A pointer to a parameter object for maintaining state between calls // // Returns: -// - []string: A slice of strings, each containing an OpenAI-compatible JSON response -func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: A slice of JSON payload chunks in OpenAI format. +func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if bytes.HasPrefix(rawJSON, []byte("data:")) { rawJSON = bytes.TrimSpace(rawJSON[5:]) } if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } - return []string{string(rawJSON)} + return [][]byte{rawJSON} } -// ConvertOpenAIResponseToOpenAINonStream converts a non-streaming Gemini CLI response to a non-streaming OpenAI response. -// This function processes the complete Gemini CLI response and transforms it into a single OpenAI-compatible -// JSON response. It handles message content, tool calls, reasoning content, and usage metadata, combining all -// the information into a single response that matches the OpenAI API format. +// ConvertOpenAIResponseToOpenAINonStream passes through a non-streaming OpenAI response. // // Parameters: // - ctx: The context for the request, used for cancellation and timeout handling @@ -46,7 +38,7 @@ func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestR // - param: A pointer to a parameter object for the conversion // // Returns: -// - string: An OpenAI-compatible JSON response containing all message content and metadata -func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { - return string(rawJSON) +// - []byte: The OpenAI-compatible JSON response. +func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { + return rawJSON } diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_request.go b/internal/translator/openai/openai/responses/openai_openai-responses_request.go index 9a64798b..2366c9c3 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_request.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_request.go @@ -29,30 +29,30 @@ import ( func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte { rawJSON := inputRawJSON // Base OpenAI chat completions template with default values - out := `{"model":"","messages":[],"stream":false}` + out := []byte(`{"model":"","messages":[],"stream":false}`) root := gjson.ParseBytes(rawJSON) // Set model name - out, _ = sjson.Set(out, "model", modelName) + out, _ = sjson.SetBytes(out, "model", modelName) // Set stream configuration - out, _ = sjson.Set(out, "stream", stream) + out, _ = sjson.SetBytes(out, "stream", stream) // Map generation parameters from responses format to chat completions format if maxTokens := root.Get("max_output_tokens"); maxTokens.Exists() { - out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } if parallelToolCalls := root.Get("parallel_tool_calls"); parallelToolCalls.Exists() { - out, _ = sjson.Set(out, "parallel_tool_calls", parallelToolCalls.Bool()) + out, _ = sjson.SetBytes(out, "parallel_tool_calls", parallelToolCalls.Bool()) } // Convert instructions to system message if instructions := root.Get("instructions"); instructions.Exists() { - systemMessage := `{"role":"system","content":""}` - systemMessage, _ = sjson.Set(systemMessage, "content", instructions.String()) - out, _ = sjson.SetRaw(out, "messages.-1", systemMessage) + systemMessage := []byte(`{"role":"system","content":""}`) + systemMessage, _ = sjson.SetBytes(systemMessage, "content", instructions.String()) + out, _ = sjson.SetRawBytes(out, "messages.-1", systemMessage) } // Convert input array to messages @@ -70,8 +70,8 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu if role == "developer" { role = "user" } - message := `{"role":"","content":[]}` - message, _ = sjson.Set(message, "role", role) + message := []byte(`{"role":"","content":[]}`) + message, _ = sjson.SetBytes(message, "role", role) if content := item.Get("content"); content.Exists() && content.IsArray() { var messageContent string @@ -86,74 +86,74 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu switch contentType { case "input_text", "output_text": text := contentItem.Get("text").String() - contentPart := `{"type":"text","text":""}` - contentPart, _ = sjson.Set(contentPart, "text", text) - message, _ = sjson.SetRaw(message, "content.-1", contentPart) + contentPart := []byte(`{"type":"text","text":""}`) + contentPart, _ = sjson.SetBytes(contentPart, "text", text) + message, _ = sjson.SetRawBytes(message, "content.-1", contentPart) case "input_image": imageURL := contentItem.Get("image_url").String() - contentPart := `{"type":"image_url","image_url":{"url":""}}` - contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL) - message, _ = sjson.SetRaw(message, "content.-1", contentPart) + contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`) + contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL) + message, _ = sjson.SetRawBytes(message, "content.-1", contentPart) } return true }) if messageContent != "" { - message, _ = sjson.Set(message, "content", messageContent) + message, _ = sjson.SetBytes(message, "content", messageContent) } if len(toolCalls) > 0 { - message, _ = sjson.Set(message, "tool_calls", toolCalls) + message, _ = sjson.SetBytes(message, "tool_calls", toolCalls) } } else if content.Type == gjson.String { - message, _ = sjson.Set(message, "content", content.String()) + message, _ = sjson.SetBytes(message, "content", content.String()) } - out, _ = sjson.SetRaw(out, "messages.-1", message) + out, _ = sjson.SetRawBytes(out, "messages.-1", message) case "function_call": // Handle function call conversion to assistant message with tool_calls - assistantMessage := `{"role":"assistant","tool_calls":[]}` + assistantMessage := []byte(`{"role":"assistant","tool_calls":[]}`) - toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}` + toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`) if callId := item.Get("call_id"); callId.Exists() { - toolCall, _ = sjson.Set(toolCall, "id", callId.String()) + toolCall, _ = sjson.SetBytes(toolCall, "id", callId.String()) } if name := item.Get("name"); name.Exists() { - toolCall, _ = sjson.Set(toolCall, "function.name", name.String()) + toolCall, _ = sjson.SetBytes(toolCall, "function.name", name.String()) } if arguments := item.Get("arguments"); arguments.Exists() { - toolCall, _ = sjson.Set(toolCall, "function.arguments", arguments.String()) + toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", arguments.String()) } - assistantMessage, _ = sjson.SetRaw(assistantMessage, "tool_calls.0", toolCall) - out, _ = sjson.SetRaw(out, "messages.-1", assistantMessage) + assistantMessage, _ = sjson.SetRawBytes(assistantMessage, "tool_calls.0", toolCall) + out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage) case "function_call_output": // Handle function call output conversion to tool message - toolMessage := `{"role":"tool","tool_call_id":"","content":""}` + toolMessage := []byte(`{"role":"tool","tool_call_id":"","content":""}`) if callId := item.Get("call_id"); callId.Exists() { - toolMessage, _ = sjson.Set(toolMessage, "tool_call_id", callId.String()) + toolMessage, _ = sjson.SetBytes(toolMessage, "tool_call_id", callId.String()) } if output := item.Get("output"); output.Exists() { - toolMessage, _ = sjson.Set(toolMessage, "content", output.String()) + toolMessage, _ = sjson.SetBytes(toolMessage, "content", output.String()) } - out, _ = sjson.SetRaw(out, "messages.-1", toolMessage) + out, _ = sjson.SetRawBytes(out, "messages.-1", toolMessage) } return true }) } else if input.Type == gjson.String { - msg := "{}" - msg, _ = sjson.Set(msg, "role", "user") - msg, _ = sjson.Set(msg, "content", input.String()) - out, _ = sjson.SetRaw(out, "messages.-1", msg) + msg := []byte(`{}`) + msg, _ = sjson.SetBytes(msg, "role", "user") + msg, _ = sjson.SetBytes(msg, "content", input.String()) + out, _ = sjson.SetRawBytes(out, "messages.-1", msg) } // Convert tools from responses format to chat completions format @@ -170,45 +170,45 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu return true } - chatTool := `{"type":"function","function":{}}` + chatTool := []byte(`{"type":"function","function":{}}`) // Convert tool structure from responses format to chat completions format - function := `{"name":"","description":"","parameters":{}}` + function := []byte(`{"name":"","description":"","parameters":{}}`) if name := tool.Get("name"); name.Exists() { - function, _ = sjson.Set(function, "name", name.String()) + function, _ = sjson.SetBytes(function, "name", name.String()) } if description := tool.Get("description"); description.Exists() { - function, _ = sjson.Set(function, "description", description.String()) + function, _ = sjson.SetBytes(function, "description", description.String()) } if parameters := tool.Get("parameters"); parameters.Exists() { - function, _ = sjson.SetRaw(function, "parameters", parameters.Raw) + function, _ = sjson.SetRawBytes(function, "parameters", []byte(parameters.Raw)) } - chatTool, _ = sjson.SetRaw(chatTool, "function", function) - chatCompletionsTools = append(chatCompletionsTools, gjson.Parse(chatTool).Value()) + chatTool, _ = sjson.SetRawBytes(chatTool, "function", function) + chatCompletionsTools = append(chatCompletionsTools, gjson.ParseBytes(chatTool).Value()) return true }) if len(chatCompletionsTools) > 0 { - out, _ = sjson.Set(out, "tools", chatCompletionsTools) + out, _ = sjson.SetBytes(out, "tools", chatCompletionsTools) } } if reasoningEffort := root.Get("reasoning.effort"); reasoningEffort.Exists() { effort := strings.ToLower(strings.TrimSpace(reasoningEffort.String())) if effort != "" { - out, _ = sjson.Set(out, "reasoning_effort", effort) + out, _ = sjson.SetBytes(out, "reasoning_effort", effort) } } // Convert tool_choice if present if toolChoice := root.Get("tool_choice"); toolChoice.Exists() { - out, _ = sjson.Set(out, "tool_choice", toolChoice.String()) + out, _ = sjson.SetBytes(out, "tool_choice", toolChoice.String()) } - return []byte(out) + return out } diff --git a/internal/translator/openai/openai/responses/openai_openai-responses_response.go b/internal/translator/openai/openai/responses/openai_openai-responses_response.go index 15152852..c2ac608a 100644 --- a/internal/translator/openai/openai/responses/openai_openai-responses_response.go +++ b/internal/translator/openai/openai/responses/openai_openai-responses_response.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -50,13 +51,13 @@ type oaiToResponsesState struct { // responseIDCounter provides a process-wide unique counter for synthesized response identifiers. var responseIDCounter uint64 -func emitRespEvent(event string, payload string) string { - return fmt.Sprintf("event: %s\ndata: %s", event, payload) +func emitRespEvent(event string, payload []byte) []byte { + return translatorcommon.SSEEventData(event, payload) } // ConvertOpenAIChatCompletionsResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks // to OpenAI Responses SSE events (response.*). -func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { if *param == nil { *param = &oaiToResponsesState{ FuncArgsBuf: make(map[int]*strings.Builder), @@ -79,19 +80,19 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, rawJSON = bytes.TrimSpace(rawJSON) if len(rawJSON) == 0 { - return []string{} + return [][]byte{} } if bytes.Equal(rawJSON, []byte("[DONE]")) { - return []string{} + return [][]byte{} } root := gjson.ParseBytes(rawJSON) obj := root.Get("object") if obj.Exists() && obj.String() != "" && obj.String() != "chat.completion.chunk" { - return []string{} + return [][]byte{} } if !root.Get("choices").Exists() || !root.Get("choices").IsArray() { - return []string{} + return [][]byte{} } if usage := root.Get("usage"); usage.Exists() { @@ -124,7 +125,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, } nextSeq := func() int { st.Seq++; return st.Seq } - var out []string + var out [][]byte if !st.Started { st.ResponseID = root.Get("id").String() @@ -149,39 +150,39 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, st.ReasoningTokens = 0 st.UsageSeen = false // response.created - created := `{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}` - created, _ = sjson.Set(created, "sequence_number", nextSeq()) - created, _ = sjson.Set(created, "response.id", st.ResponseID) - created, _ = sjson.Set(created, "response.created_at", st.Created) + created := []byte(`{"type":"response.created","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress","background":false,"error":null,"output":[]}}`) + created, _ = sjson.SetBytes(created, "sequence_number", nextSeq()) + created, _ = sjson.SetBytes(created, "response.id", st.ResponseID) + created, _ = sjson.SetBytes(created, "response.created_at", st.Created) out = append(out, emitRespEvent("response.created", created)) - inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}` - inprog, _ = sjson.Set(inprog, "sequence_number", nextSeq()) - inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID) - inprog, _ = sjson.Set(inprog, "response.created_at", st.Created) + inprog := []byte(`{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`) + inprog, _ = sjson.SetBytes(inprog, "sequence_number", nextSeq()) + inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID) + inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.Created) out = append(out, emitRespEvent("response.in_progress", inprog)) st.Started = true } stopReasoning := func(text string) { // Emit reasoning done events - textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}` - textDone, _ = sjson.Set(textDone, "sequence_number", nextSeq()) - textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID) - textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex) - textDone, _ = sjson.Set(textDone, "text", text) + textDone := []byte(`{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`) + textDone, _ = sjson.SetBytes(textDone, "sequence_number", nextSeq()) + textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningID) + textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex) + textDone, _ = sjson.SetBytes(textDone, "text", text) out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone)) - partDone := `{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID) - partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex) - partDone, _ = sjson.Set(partDone, "part.text", text) + partDone := []byte(`{"type":"response.reasoning_summary_part.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningID) + partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex) + partDone, _ = sjson.SetBytes(partDone, "part.text", text) out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone)) - outputItemDone := `{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}` - outputItemDone, _ = sjson.Set(outputItemDone, "sequence_number", nextSeq()) - outputItemDone, _ = sjson.Set(outputItemDone, "item.id", st.ReasoningID) - outputItemDone, _ = sjson.Set(outputItemDone, "output_index", st.ReasoningIndex) - outputItemDone, _ = sjson.Set(outputItemDone, "item.summary.text", text) + outputItemDone := []byte(`{"type":"response.output_item.done","item":{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]},"output_index":0,"sequence_number":0}`) + outputItemDone, _ = sjson.SetBytes(outputItemDone, "sequence_number", nextSeq()) + outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.id", st.ReasoningID) + outputItemDone, _ = sjson.SetBytes(outputItemDone, "output_index", st.ReasoningIndex) + outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.summary.text", text) out = append(out, emitRespEvent("response.output_item.done", outputItemDone)) st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text}) @@ -201,29 +202,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, st.ReasoningBuf.Reset() } if !st.MsgItemAdded[idx] { - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", idx) - item, _ = sjson.Set(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"in_progress","content":[],"role":"assistant"}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) out = append(out, emitRespEvent("response.output_item.added", item)) st.MsgItemAdded[idx] = true } if !st.MsgContentAdded[idx] { - part := `{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - part, _ = sjson.Set(part, "sequence_number", nextSeq()) - part, _ = sjson.Set(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - part, _ = sjson.Set(part, "output_index", idx) - part, _ = sjson.Set(part, "content_index", 0) + part := []byte(`{"type":"response.content_part.added","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + part, _ = sjson.SetBytes(part, "sequence_number", nextSeq()) + part, _ = sjson.SetBytes(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) + part, _ = sjson.SetBytes(part, "output_index", idx) + part, _ = sjson.SetBytes(part, "content_index", 0) out = append(out, emitRespEvent("response.content_part.added", part)) st.MsgContentAdded[idx] = true } - msg := `{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - msg, _ = sjson.Set(msg, "output_index", idx) - msg, _ = sjson.Set(msg, "content_index", 0) - msg, _ = sjson.Set(msg, "delta", c.String()) + msg := []byte(`{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) + msg, _ = sjson.SetBytes(msg, "output_index", idx) + msg, _ = sjson.SetBytes(msg, "content_index", 0) + msg, _ = sjson.SetBytes(msg, "delta", c.String()) out = append(out, emitRespEvent("response.output_text.delta", msg)) // aggregate for response.output if st.MsgTextBuf[idx] == nil { @@ -238,24 +239,24 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, if st.ReasoningID == "" { st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx) st.ReasoningIndex = idx - item := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}` - item, _ = sjson.Set(item, "sequence_number", nextSeq()) - item, _ = sjson.Set(item, "output_index", idx) - item, _ = sjson.Set(item, "item.id", st.ReasoningID) + item := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"reasoning","status":"in_progress","summary":[]}}`) + item, _ = sjson.SetBytes(item, "sequence_number", nextSeq()) + item, _ = sjson.SetBytes(item, "output_index", idx) + item, _ = sjson.SetBytes(item, "item.id", st.ReasoningID) out = append(out, emitRespEvent("response.output_item.added", item)) - part := `{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}` - part, _ = sjson.Set(part, "sequence_number", nextSeq()) - part, _ = sjson.Set(part, "item_id", st.ReasoningID) - part, _ = sjson.Set(part, "output_index", st.ReasoningIndex) + part := []byte(`{"type":"response.reasoning_summary_part.added","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"part":{"type":"summary_text","text":""}}`) + part, _ = sjson.SetBytes(part, "sequence_number", nextSeq()) + part, _ = sjson.SetBytes(part, "item_id", st.ReasoningID) + part, _ = sjson.SetBytes(part, "output_index", st.ReasoningIndex) out = append(out, emitRespEvent("response.reasoning_summary_part.added", part)) } // Append incremental text to reasoning buffer st.ReasoningBuf.WriteString(rc.String()) - msg := `{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}` - msg, _ = sjson.Set(msg, "sequence_number", nextSeq()) - msg, _ = sjson.Set(msg, "item_id", st.ReasoningID) - msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex) - msg, _ = sjson.Set(msg, "delta", rc.String()) + msg := []byte(`{"type":"response.reasoning_summary_text.delta","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"delta":""}`) + msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq()) + msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningID) + msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex) + msg, _ = sjson.SetBytes(msg, "delta", rc.String()) out = append(out, emitRespEvent("response.reasoning_summary_text.delta", msg)) } @@ -272,27 +273,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, if b := st.MsgTextBuf[idx]; b != nil { fullText = b.String() } - done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}` - done, _ = sjson.Set(done, "sequence_number", nextSeq()) - done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - done, _ = sjson.Set(done, "output_index", idx) - done, _ = sjson.Set(done, "content_index", 0) - done, _ = sjson.Set(done, "text", fullText) + done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) + done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) + done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) + done, _ = sjson.SetBytes(done, "output_index", idx) + done, _ = sjson.SetBytes(done, "content_index", 0) + done, _ = sjson.SetBytes(done, "text", fullText) out = append(out, emitRespEvent("response.output_text.done", done)) - partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - partDone, _ = sjson.Set(partDone, "output_index", idx) - partDone, _ = sjson.Set(partDone, "content_index", 0) - partDone, _ = sjson.Set(partDone, "part.text", fullText) + partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) + partDone, _ = sjson.SetBytes(partDone, "output_index", idx) + partDone, _ = sjson.SetBytes(partDone, "content_index", 0) + partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) out = append(out, emitRespEvent("response.content_part.done", partDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "output_index", idx) - itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) - itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx)) + itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText) out = append(out, emitRespEvent("response.output_item.done", itemDone)) st.MsgItemDone[idx] = true } @@ -314,13 +315,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, } if shouldEmitItem && effectiveCallID != "" { - o := `{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}` - o, _ = sjson.Set(o, "sequence_number", nextSeq()) - o, _ = sjson.Set(o, "output_index", idx) - o, _ = sjson.Set(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID)) - o, _ = sjson.Set(o, "item.call_id", effectiveCallID) + o := []byte(`{"type":"response.output_item.added","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"in_progress","arguments":"","call_id":"","name":""}}`) + o, _ = sjson.SetBytes(o, "sequence_number", nextSeq()) + o, _ = sjson.SetBytes(o, "output_index", idx) + o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID)) + o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID) name := st.FuncNames[idx] - o, _ = sjson.Set(o, "item.name", name) + o, _ = sjson.SetBytes(o, "item.name", name) out = append(out, emitRespEvent("response.output_item.added", o)) } @@ -337,11 +338,11 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, refCallID = newCallID } if refCallID != "" { - ad := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}` - ad, _ = sjson.Set(ad, "sequence_number", nextSeq()) - ad, _ = sjson.Set(ad, "item_id", fmt.Sprintf("fc_%s", refCallID)) - ad, _ = sjson.Set(ad, "output_index", idx) - ad, _ = sjson.Set(ad, "delta", args.String()) + ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`) + ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq()) + ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID)) + ad, _ = sjson.SetBytes(ad, "output_index", idx) + ad, _ = sjson.SetBytes(ad, "delta", args.String()) out = append(out, emitRespEvent("response.function_call_arguments.delta", ad)) } st.FuncArgsBuf[idx].WriteString(args.String()) @@ -372,27 +373,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, if b := st.MsgTextBuf[i]; b != nil { fullText = b.String() } - done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}` - done, _ = sjson.Set(done, "sequence_number", nextSeq()) - done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) - done, _ = sjson.Set(done, "output_index", i) - done, _ = sjson.Set(done, "content_index", 0) - done, _ = sjson.Set(done, "text", fullText) + done := []byte(`{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`) + done, _ = sjson.SetBytes(done, "sequence_number", nextSeq()) + done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) + done, _ = sjson.SetBytes(done, "output_index", i) + done, _ = sjson.SetBytes(done, "content_index", 0) + done, _ = sjson.SetBytes(done, "text", fullText) out = append(out, emitRespEvent("response.output_text.done", done)) - partDone := `{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}` - partDone, _ = sjson.Set(partDone, "sequence_number", nextSeq()) - partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) - partDone, _ = sjson.Set(partDone, "output_index", i) - partDone, _ = sjson.Set(partDone, "content_index", 0) - partDone, _ = sjson.Set(partDone, "part.text", fullText) + partDone := []byte(`{"type":"response.content_part.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}`) + partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq()) + partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) + partDone, _ = sjson.SetBytes(partDone, "output_index", i) + partDone, _ = sjson.SetBytes(partDone, "content_index", 0) + partDone, _ = sjson.SetBytes(partDone, "part.text", fullText) out = append(out, emitRespEvent("response.content_part.done", partDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "output_index", i) - itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) - itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", i) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) + itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText) out = append(out, emitRespEvent("response.output_item.done", itemDone)) st.MsgItemDone[i] = true } @@ -426,101 +427,101 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 { args = b.String() } - fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}` - fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq()) - fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", callID)) - fcDone, _ = sjson.Set(fcDone, "output_index", i) - fcDone, _ = sjson.Set(fcDone, "arguments", args) + fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`) + fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq()) + fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", callID)) + fcDone, _ = sjson.SetBytes(fcDone, "output_index", i) + fcDone, _ = sjson.SetBytes(fcDone, "arguments", args) out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone)) - itemDone := `{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}` - itemDone, _ = sjson.Set(itemDone, "sequence_number", nextSeq()) - itemDone, _ = sjson.Set(itemDone, "output_index", i) - itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", callID)) - itemDone, _ = sjson.Set(itemDone, "item.arguments", args) - itemDone, _ = sjson.Set(itemDone, "item.call_id", callID) - itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[i]) + itemDone := []byte(`{"type":"response.output_item.done","sequence_number":0,"output_index":0,"item":{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}}`) + itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq()) + itemDone, _ = sjson.SetBytes(itemDone, "output_index", i) + itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", callID)) + itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args) + itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", callID) + itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[i]) out = append(out, emitRespEvent("response.output_item.done", itemDone)) st.FuncItemDone[i] = true st.FuncArgsDone[i] = true } } - completed := `{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}` - completed, _ = sjson.Set(completed, "sequence_number", nextSeq()) - completed, _ = sjson.Set(completed, "response.id", st.ResponseID) - completed, _ = sjson.Set(completed, "response.created_at", st.Created) + completed := []byte(`{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`) + completed, _ = sjson.SetBytes(completed, "sequence_number", nextSeq()) + completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID) + completed, _ = sjson.SetBytes(completed, "response.created_at", st.Created) // Inject original request fields into response as per docs/response.completed.json if requestRawJSON != nil { req := gjson.ParseBytes(requestRawJSON) if v := req.Get("instructions"); v.Exists() { - completed, _ = sjson.Set(completed, "response.instructions", v.String()) + completed, _ = sjson.SetBytes(completed, "response.instructions", v.String()) } if v := req.Get("max_output_tokens"); v.Exists() { - completed, _ = sjson.Set(completed, "response.max_output_tokens", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.max_output_tokens", v.Int()) } if v := req.Get("max_tool_calls"); v.Exists() { - completed, _ = sjson.Set(completed, "response.max_tool_calls", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.max_tool_calls", v.Int()) } if v := req.Get("model"); v.Exists() { - completed, _ = sjson.Set(completed, "response.model", v.String()) + completed, _ = sjson.SetBytes(completed, "response.model", v.String()) } if v := req.Get("parallel_tool_calls"); v.Exists() { - completed, _ = sjson.Set(completed, "response.parallel_tool_calls", v.Bool()) + completed, _ = sjson.SetBytes(completed, "response.parallel_tool_calls", v.Bool()) } if v := req.Get("previous_response_id"); v.Exists() { - completed, _ = sjson.Set(completed, "response.previous_response_id", v.String()) + completed, _ = sjson.SetBytes(completed, "response.previous_response_id", v.String()) } if v := req.Get("prompt_cache_key"); v.Exists() { - completed, _ = sjson.Set(completed, "response.prompt_cache_key", v.String()) + completed, _ = sjson.SetBytes(completed, "response.prompt_cache_key", v.String()) } if v := req.Get("reasoning"); v.Exists() { - completed, _ = sjson.Set(completed, "response.reasoning", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.reasoning", v.Value()) } if v := req.Get("safety_identifier"); v.Exists() { - completed, _ = sjson.Set(completed, "response.safety_identifier", v.String()) + completed, _ = sjson.SetBytes(completed, "response.safety_identifier", v.String()) } if v := req.Get("service_tier"); v.Exists() { - completed, _ = sjson.Set(completed, "response.service_tier", v.String()) + completed, _ = sjson.SetBytes(completed, "response.service_tier", v.String()) } if v := req.Get("store"); v.Exists() { - completed, _ = sjson.Set(completed, "response.store", v.Bool()) + completed, _ = sjson.SetBytes(completed, "response.store", v.Bool()) } if v := req.Get("temperature"); v.Exists() { - completed, _ = sjson.Set(completed, "response.temperature", v.Float()) + completed, _ = sjson.SetBytes(completed, "response.temperature", v.Float()) } if v := req.Get("text"); v.Exists() { - completed, _ = sjson.Set(completed, "response.text", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.text", v.Value()) } if v := req.Get("tool_choice"); v.Exists() { - completed, _ = sjson.Set(completed, "response.tool_choice", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.tool_choice", v.Value()) } if v := req.Get("tools"); v.Exists() { - completed, _ = sjson.Set(completed, "response.tools", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.tools", v.Value()) } if v := req.Get("top_logprobs"); v.Exists() { - completed, _ = sjson.Set(completed, "response.top_logprobs", v.Int()) + completed, _ = sjson.SetBytes(completed, "response.top_logprobs", v.Int()) } if v := req.Get("top_p"); v.Exists() { - completed, _ = sjson.Set(completed, "response.top_p", v.Float()) + completed, _ = sjson.SetBytes(completed, "response.top_p", v.Float()) } if v := req.Get("truncation"); v.Exists() { - completed, _ = sjson.Set(completed, "response.truncation", v.String()) + completed, _ = sjson.SetBytes(completed, "response.truncation", v.String()) } if v := req.Get("user"); v.Exists() { - completed, _ = sjson.Set(completed, "response.user", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.user", v.Value()) } if v := req.Get("metadata"); v.Exists() { - completed, _ = sjson.Set(completed, "response.metadata", v.Value()) + completed, _ = sjson.SetBytes(completed, "response.metadata", v.Value()) } } // Build response.output using aggregated buffers - outputsWrapper := `{"arr":[]}` + outputsWrapper := []byte(`{"arr":[]}`) if len(st.Reasonings) > 0 { for _, r := range st.Reasonings { - item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}` - item, _ = sjson.Set(item, "id", r.ReasoningID) - item, _ = sjson.Set(item, "summary.0.text", r.ReasoningData) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`) + item, _ = sjson.SetBytes(item, "id", r.ReasoningID) + item, _ = sjson.SetBytes(item, "summary.0.text", r.ReasoningData) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } } // Append message items in ascending index order @@ -541,10 +542,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, if b := st.MsgTextBuf[i]; b != nil { txt = b.String() } - item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) - item, _ = sjson.Set(item, "content.0.text", txt) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i)) + item, _ = sjson.SetBytes(item, "content.0.text", txt) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } } if len(st.FuncArgsBuf) > 0 { @@ -567,29 +568,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, } callID := st.FuncCallIDs[i] name := st.FuncNames[i] - item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) - item, _ = sjson.Set(item, "arguments", args) - item, _ = sjson.Set(item, "call_id", callID) - item, _ = sjson.Set(item, "name", name) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID)) + item, _ = sjson.SetBytes(item, "arguments", args) + item, _ = sjson.SetBytes(item, "call_id", callID) + item, _ = sjson.SetBytes(item, "name", name) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } } - if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { - completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw) + if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { + completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) } if st.UsageSeen { - completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens) - completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens) - completed, _ = sjson.Set(completed, "response.usage.output_tokens", st.CompletionTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", st.PromptTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens", st.CompletionTokens) if st.ReasoningTokens > 0 { - completed, _ = sjson.Set(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens) + completed, _ = sjson.SetBytes(completed, "response.usage.output_tokens_details.reasoning_tokens", st.ReasoningTokens) } total := st.TotalTokens if total == 0 { total = st.PromptTokens + st.CompletionTokens } - completed, _ = sjson.Set(completed, "response.usage.total_tokens", total) + completed, _ = sjson.SetBytes(completed, "response.usage.total_tokens", total) } out = append(out, emitRespEvent("response.completed", completed)) } @@ -603,103 +604,103 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context, // ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream builds a single Responses JSON // from a non-streaming OpenAI Chat Completions response. -func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string { +func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte { root := gjson.ParseBytes(rawJSON) // Basic response scaffold - resp := `{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}` + resp := []byte(`{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null,"incomplete_details":null}`) // id: use provider id if present, otherwise synthesize id := root.Get("id").String() if id == "" { id = fmt.Sprintf("resp_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&responseIDCounter, 1)) } - resp, _ = sjson.Set(resp, "id", id) + resp, _ = sjson.SetBytes(resp, "id", id) // created_at: map from chat.completion created created := root.Get("created").Int() if created == 0 { created = time.Now().Unix() } - resp, _ = sjson.Set(resp, "created_at", created) + resp, _ = sjson.SetBytes(resp, "created_at", created) // Echo request fields when available (aligns with streaming path behavior) if len(requestRawJSON) > 0 { req := gjson.ParseBytes(requestRawJSON) if v := req.Get("instructions"); v.Exists() { - resp, _ = sjson.Set(resp, "instructions", v.String()) + resp, _ = sjson.SetBytes(resp, "instructions", v.String()) } if v := req.Get("max_output_tokens"); v.Exists() { - resp, _ = sjson.Set(resp, "max_output_tokens", v.Int()) + resp, _ = sjson.SetBytes(resp, "max_output_tokens", v.Int()) } else { // Also support max_tokens from chat completion style if v = req.Get("max_tokens"); v.Exists() { - resp, _ = sjson.Set(resp, "max_output_tokens", v.Int()) + resp, _ = sjson.SetBytes(resp, "max_output_tokens", v.Int()) } } if v := req.Get("max_tool_calls"); v.Exists() { - resp, _ = sjson.Set(resp, "max_tool_calls", v.Int()) + resp, _ = sjson.SetBytes(resp, "max_tool_calls", v.Int()) } if v := req.Get("model"); v.Exists() { - resp, _ = sjson.Set(resp, "model", v.String()) + resp, _ = sjson.SetBytes(resp, "model", v.String()) } else if v = root.Get("model"); v.Exists() { - resp, _ = sjson.Set(resp, "model", v.String()) + resp, _ = sjson.SetBytes(resp, "model", v.String()) } if v := req.Get("parallel_tool_calls"); v.Exists() { - resp, _ = sjson.Set(resp, "parallel_tool_calls", v.Bool()) + resp, _ = sjson.SetBytes(resp, "parallel_tool_calls", v.Bool()) } if v := req.Get("previous_response_id"); v.Exists() { - resp, _ = sjson.Set(resp, "previous_response_id", v.String()) + resp, _ = sjson.SetBytes(resp, "previous_response_id", v.String()) } if v := req.Get("prompt_cache_key"); v.Exists() { - resp, _ = sjson.Set(resp, "prompt_cache_key", v.String()) + resp, _ = sjson.SetBytes(resp, "prompt_cache_key", v.String()) } if v := req.Get("reasoning"); v.Exists() { - resp, _ = sjson.Set(resp, "reasoning", v.Value()) + resp, _ = sjson.SetBytes(resp, "reasoning", v.Value()) } if v := req.Get("safety_identifier"); v.Exists() { - resp, _ = sjson.Set(resp, "safety_identifier", v.String()) + resp, _ = sjson.SetBytes(resp, "safety_identifier", v.String()) } if v := req.Get("service_tier"); v.Exists() { - resp, _ = sjson.Set(resp, "service_tier", v.String()) + resp, _ = sjson.SetBytes(resp, "service_tier", v.String()) } if v := req.Get("store"); v.Exists() { - resp, _ = sjson.Set(resp, "store", v.Bool()) + resp, _ = sjson.SetBytes(resp, "store", v.Bool()) } if v := req.Get("temperature"); v.Exists() { - resp, _ = sjson.Set(resp, "temperature", v.Float()) + resp, _ = sjson.SetBytes(resp, "temperature", v.Float()) } if v := req.Get("text"); v.Exists() { - resp, _ = sjson.Set(resp, "text", v.Value()) + resp, _ = sjson.SetBytes(resp, "text", v.Value()) } if v := req.Get("tool_choice"); v.Exists() { - resp, _ = sjson.Set(resp, "tool_choice", v.Value()) + resp, _ = sjson.SetBytes(resp, "tool_choice", v.Value()) } if v := req.Get("tools"); v.Exists() { - resp, _ = sjson.Set(resp, "tools", v.Value()) + resp, _ = sjson.SetBytes(resp, "tools", v.Value()) } if v := req.Get("top_logprobs"); v.Exists() { - resp, _ = sjson.Set(resp, "top_logprobs", v.Int()) + resp, _ = sjson.SetBytes(resp, "top_logprobs", v.Int()) } if v := req.Get("top_p"); v.Exists() { - resp, _ = sjson.Set(resp, "top_p", v.Float()) + resp, _ = sjson.SetBytes(resp, "top_p", v.Float()) } if v := req.Get("truncation"); v.Exists() { - resp, _ = sjson.Set(resp, "truncation", v.String()) + resp, _ = sjson.SetBytes(resp, "truncation", v.String()) } if v := req.Get("user"); v.Exists() { - resp, _ = sjson.Set(resp, "user", v.Value()) + resp, _ = sjson.SetBytes(resp, "user", v.Value()) } if v := req.Get("metadata"); v.Exists() { - resp, _ = sjson.Set(resp, "metadata", v.Value()) + resp, _ = sjson.SetBytes(resp, "metadata", v.Value()) } } else if v := root.Get("model"); v.Exists() { // Fallback model from response - resp, _ = sjson.Set(resp, "model", v.String()) + resp, _ = sjson.SetBytes(resp, "model", v.String()) } // Build output list from choices[...] - outputsWrapper := `{"arr":[]}` + outputsWrapper := []byte(`{"arr":[]}`) // Detect and capture reasoning content if present rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String() includeReasoning := rcText != "" @@ -712,13 +713,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co rid = strings.TrimPrefix(rid, "resp_") } // Prefer summary_text from reasoning_content; encrypted_content is optional - reasoningItem := `{"id":"","type":"reasoning","encrypted_content":"","summary":[]}` - reasoningItem, _ = sjson.Set(reasoningItem, "id", fmt.Sprintf("rs_%s", rid)) + reasoningItem := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`) + reasoningItem, _ = sjson.SetBytes(reasoningItem, "id", fmt.Sprintf("rs_%s", rid)) if rcText != "" { - reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text") - reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText) + reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.type", "summary_text") + reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.text", rcText) } - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", reasoningItem) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", reasoningItem) } if choices := root.Get("choices"); choices.Exists() && choices.IsArray() { @@ -727,10 +728,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co if msg.Exists() { // Text message part if c := msg.Get("content"); c.Exists() && c.String() != "" { - item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int()))) - item, _ = sjson.Set(item, "content.0.text", c.String()) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int()))) + item, _ = sjson.SetBytes(item, "content.0.text", c.String()) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) } // Function/tool calls @@ -739,12 +740,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co callID := tc.Get("id").String() name := tc.Get("function.name").String() args := tc.Get("function.arguments").String() - item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}` - item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID)) - item, _ = sjson.Set(item, "arguments", args) - item, _ = sjson.Set(item, "call_id", callID) - item, _ = sjson.Set(item, "name", name) - outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item) + item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`) + item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID)) + item, _ = sjson.SetBytes(item, "arguments", args) + item, _ = sjson.SetBytes(item, "call_id", callID) + item, _ = sjson.SetBytes(item, "name", name) + outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item) return true }) } @@ -752,27 +753,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co return true }) } - if gjson.Get(outputsWrapper, "arr.#").Int() > 0 { - resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw) + if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 { + resp, _ = sjson.SetRawBytes(resp, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw)) } // usage mapping if usage := root.Get("usage"); usage.Exists() { // Map common tokens if usage.Get("prompt_tokens").Exists() || usage.Get("completion_tokens").Exists() || usage.Get("total_tokens").Exists() { - resp, _ = sjson.Set(resp, "usage.input_tokens", usage.Get("prompt_tokens").Int()) + resp, _ = sjson.SetBytes(resp, "usage.input_tokens", usage.Get("prompt_tokens").Int()) if d := usage.Get("prompt_tokens_details.cached_tokens"); d.Exists() { - resp, _ = sjson.Set(resp, "usage.input_tokens_details.cached_tokens", d.Int()) + resp, _ = sjson.SetBytes(resp, "usage.input_tokens_details.cached_tokens", d.Int()) } - resp, _ = sjson.Set(resp, "usage.output_tokens", usage.Get("completion_tokens").Int()) + resp, _ = sjson.SetBytes(resp, "usage.output_tokens", usage.Get("completion_tokens").Int()) // Reasoning tokens not available in Chat Completions; set only if present under output_tokens_details if d := usage.Get("output_tokens_details.reasoning_tokens"); d.Exists() { - resp, _ = sjson.Set(resp, "usage.output_tokens_details.reasoning_tokens", d.Int()) + resp, _ = sjson.SetBytes(resp, "usage.output_tokens_details.reasoning_tokens", d.Int()) } - resp, _ = sjson.Set(resp, "usage.total_tokens", usage.Get("total_tokens").Int()) + resp, _ = sjson.SetBytes(resp, "usage.total_tokens", usage.Get("total_tokens").Int()) } else { // Fallback to raw usage object if structure differs - resp, _ = sjson.Set(resp, "usage", usage.Value()) + resp, _ = sjson.SetBytes(resp, "usage", usage.Value()) } } diff --git a/internal/translator/translator/translator.go b/internal/translator/translator/translator.go index 11a881ad..ab3f68a9 100644 --- a/internal/translator/translator/translator.go +++ b/internal/translator/translator/translator.go @@ -65,8 +65,8 @@ func NeedConvert(from, to string) bool { // - param: Additional parameters for translation // // Returns: -// - []string: The translated response lines -func Response(from, to string, ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +// - [][]byte: The translated response lines +func Response(from, to string, ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { return registry.TranslateStream(ctx, sdktranslator.FromString(from), sdktranslator.FromString(to), modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) } @@ -83,7 +83,7 @@ func Response(from, to string, ctx context.Context, modelName string, originalRe // - param: Additional parameters for translation // // Returns: -// - string: The translated response JSON -func ResponseNonStream(from, to string, ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +// - []byte: The translated response JSON +func ResponseNonStream(from, to string, ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { return registry.TranslateNonStream(ctx, sdktranslator.FromString(from), sdktranslator.FromString(to), modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param) } diff --git a/internal/util/gemini_schema.go b/internal/util/gemini_schema.go index 8617b846..4cc946d5 100644 --- a/internal/util/gemini_schema.go +++ b/internal/util/gemini_schema.go @@ -101,7 +101,8 @@ func removePlaceholderFields(jsonStr string) string { if len(filtered) == 0 { jsonStr, _ = sjson.Delete(jsonStr, reqPath) } else { - jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) + updated, _ := sjson.SetBytes([]byte(jsonStr), reqPath, filtered) + jsonStr = string(updated) } } } @@ -135,7 +136,8 @@ func removePlaceholderFields(jsonStr string) string { if len(filtered) == 0 { jsonStr, _ = sjson.Delete(jsonStr, reqPath) } else { - jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) + updated, _ := sjson.SetBytes([]byte(jsonStr), reqPath, filtered) + jsonStr = string(updated) } } } @@ -162,7 +164,8 @@ func convertRefsToHints(jsonStr string) string { } replacement := `{"type":"object","description":""}` - replacement, _ = sjson.Set(replacement, "description", hint) + replacementBytes, _ := sjson.SetBytes([]byte(replacement), "description", hint) + replacement = string(replacementBytes) jsonStr = setRawAt(jsonStr, parentPath, replacement) } return jsonStr @@ -176,7 +179,8 @@ func convertConstToEnum(jsonStr string) string { } enumPath := trimSuffix(p, ".const") + ".enum" if !gjson.Get(jsonStr, enumPath).Exists() { - jsonStr, _ = sjson.Set(jsonStr, enumPath, []interface{}{val.Value()}) + updated, _ := sjson.SetBytes([]byte(jsonStr), enumPath, []interface{}{val.Value()}) + jsonStr = string(updated) } } return jsonStr @@ -198,9 +202,11 @@ func convertEnumValuesToStrings(jsonStr string) string { // Always update enum values to strings and set type to "string" // This ensures compatibility with Antigravity Gemini which only allows enum for STRING type - jsonStr, _ = sjson.Set(jsonStr, p, stringVals) + updated, _ := sjson.SetBytes([]byte(jsonStr), p, stringVals) + jsonStr = string(updated) parentPath := trimSuffix(p, ".enum") - jsonStr, _ = sjson.Set(jsonStr, joinPath(parentPath, "type"), "string") + updated, _ = sjson.SetBytes([]byte(jsonStr), joinPath(parentPath, "type"), "string") + jsonStr = string(updated) } return jsonStr } @@ -236,7 +242,7 @@ func addAdditionalPropertiesHints(jsonStr string) string { var unsupportedConstraints = []string{ "minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum", - "pattern", "minItems", "maxItems", "format", + "pattern", "minItems", "maxItems", "uniqueItems", "format", "default", "examples", // Claude rejects these in VALIDATED mode } @@ -273,7 +279,8 @@ func mergeAllOf(jsonStr string) string { if props := item.Get("properties"); props.IsObject() { props.ForEach(func(key, value gjson.Result) bool { destPath := joinPath(parentPath, "properties."+escapeGJSONPathKey(key.String())) - jsonStr, _ = sjson.SetRaw(jsonStr, destPath, value.Raw) + updated, _ := sjson.SetRawBytes([]byte(jsonStr), destPath, []byte(value.Raw)) + jsonStr = string(updated) return true }) } @@ -285,7 +292,8 @@ func mergeAllOf(jsonStr string) string { current = append(current, s) } } - jsonStr, _ = sjson.Set(jsonStr, reqPath, current) + updated, _ := sjson.SetBytes([]byte(jsonStr), reqPath, current) + jsonStr = string(updated) } } jsonStr, _ = sjson.Delete(jsonStr, p) @@ -381,7 +389,8 @@ func flattenTypeArrays(jsonStr string) string { firstType = nonNullTypes[0] } - jsonStr, _ = sjson.Set(jsonStr, p, firstType) + updated, _ := sjson.SetBytes([]byte(jsonStr), p, firstType) + jsonStr = string(updated) parentPath := trimSuffix(p, ".type") if len(nonNullTypes) > 1 { @@ -420,7 +429,8 @@ func flattenTypeArrays(jsonStr string) string { if len(filtered) == 0 { jsonStr, _ = sjson.Delete(jsonStr, reqPath) } else { - jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered) + updated, _ := sjson.SetBytes([]byte(jsonStr), reqPath, filtered) + jsonStr = string(updated) } } return jsonStr @@ -518,7 +528,8 @@ func cleanupRequiredFields(jsonStr string) string { if len(valid) == 0 { jsonStr, _ = sjson.Delete(jsonStr, p) } else { - jsonStr, _ = sjson.Set(jsonStr, p, valid) + updated, _ := sjson.SetBytes([]byte(jsonStr), p, valid) + jsonStr = string(updated) } } } @@ -562,11 +573,14 @@ func addEmptySchemaPlaceholder(jsonStr string) string { if needsPlaceholder { // Add placeholder "reason" property reasonPath := joinPath(propsPath, "reason") - jsonStr, _ = sjson.Set(jsonStr, reasonPath+".type", "string") - jsonStr, _ = sjson.Set(jsonStr, reasonPath+".description", placeholderReasonDescription) + updated, _ := sjson.SetBytes([]byte(jsonStr), reasonPath+".type", "string") + jsonStr = string(updated) + updated, _ = sjson.SetBytes([]byte(jsonStr), reasonPath+".description", placeholderReasonDescription) + jsonStr = string(updated) // Add to required array - jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"reason"}) + updated, _ = sjson.SetBytes([]byte(jsonStr), reqPath, []string{"reason"}) + jsonStr = string(updated) continue } @@ -579,9 +593,11 @@ func addEmptySchemaPlaceholder(jsonStr string) string { } placeholderPath := joinPath(propsPath, "_") if !gjson.Get(jsonStr, placeholderPath).Exists() { - jsonStr, _ = sjson.Set(jsonStr, placeholderPath+".type", "boolean") + updated, _ := sjson.SetBytes([]byte(jsonStr), placeholderPath+".type", "boolean") + jsonStr = string(updated) } - jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"_"}) + updated, _ := sjson.SetBytes([]byte(jsonStr), reqPath, []string{"_"}) + jsonStr = string(updated) } } @@ -654,8 +670,8 @@ func setRawAt(jsonStr, path, value string) string { if path == "" { return value } - result, _ := sjson.SetRaw(jsonStr, path, value) - return result + result, _ := sjson.SetRawBytes([]byte(jsonStr), path, []byte(value)) + return string(result) } func isPropertyDefinition(path string) bool { @@ -678,7 +694,8 @@ func appendHint(jsonStr, parentPath, hint string) string { if existing != "" { hint = fmt.Sprintf("%s (%s)", existing, hint) } - jsonStr, _ = sjson.Set(jsonStr, descPath, hint) + updated, _ := sjson.SetBytes([]byte(jsonStr), descPath, hint) + jsonStr = string(updated) return jsonStr } @@ -687,7 +704,8 @@ func appendHintRaw(jsonRaw, hint string) string { if existing != "" { hint = fmt.Sprintf("%s (%s)", existing, hint) } - jsonRaw, _ = sjson.Set(jsonRaw, "description", hint) + updated, _ := sjson.SetBytes([]byte(jsonRaw), "description", hint) + jsonRaw = string(updated) return jsonRaw } @@ -773,13 +791,13 @@ func mergeDescriptionRaw(schemaRaw, parentDesc string) string { childDesc := gjson.Get(schemaRaw, "description").String() switch { case childDesc == "": - schemaRaw, _ = sjson.Set(schemaRaw, "description", parentDesc) - return schemaRaw + updated, _ := sjson.SetBytes([]byte(schemaRaw), "description", parentDesc) + return string(updated) case childDesc == parentDesc: return schemaRaw default: combined := fmt.Sprintf("%s (%s)", parentDesc, childDesc) - schemaRaw, _ = sjson.Set(schemaRaw, "description", combined) - return schemaRaw + updated, _ := sjson.SetBytes([]byte(schemaRaw), "description", combined) + return string(updated) } } diff --git a/internal/util/gemini_schema_test.go b/internal/util/gemini_schema_test.go index bb06e956..92bce013 100644 --- a/internal/util/gemini_schema_test.go +++ b/internal/util/gemini_schema_test.go @@ -1046,3 +1046,27 @@ func TestRemoveExtensionFields(t *testing.T) { }) } } + +// uniqueItems should be stripped and moved to description hint (#2123). +func TestCleanJSONSchemaForAntigravity_UniqueItemsStripped(t *testing.T) { + input := `{ + "type": "object", + "properties": { + "ids": { + "type": "array", + "description": "Unique identifiers", + "items": {"type": "string"}, + "uniqueItems": true + } + } + }` + + result := CleanJSONSchemaForAntigravity(input) + + if strings.Contains(result, `"uniqueItems"`) { + t.Errorf("uniqueItems should be removed from schema") + } + if !strings.Contains(result, "uniqueItems: true") { + t.Errorf("uniqueItems hint missing in description") + } +} diff --git a/internal/util/translator.go b/internal/util/translator.go index 669ba745..4a1a1d80 100644 --- a/internal/util/translator.go +++ b/internal/util/translator.go @@ -74,17 +74,17 @@ func RenameKey(jsonStr, oldKeyPath, newKeyPath string) (string, error) { return "", fmt.Errorf("old key '%s' does not exist", oldKeyPath) } - interimJson, err := sjson.SetRaw(jsonStr, newKeyPath, value.Raw) - if err != nil { - return "", fmt.Errorf("failed to set new key '%s': %w", newKeyPath, err) + interimJSON, errSet := sjson.SetRawBytes([]byte(jsonStr), newKeyPath, []byte(value.Raw)) + if errSet != nil { + return "", fmt.Errorf("failed to set new key '%s': %w", newKeyPath, errSet) } - finalJson, err := sjson.Delete(interimJson, oldKeyPath) - if err != nil { - return "", fmt.Errorf("failed to delete old key '%s': %w", oldKeyPath, err) + finalJSON, errDelete := sjson.DeleteBytes(interimJSON, oldKeyPath) + if errDelete != nil { + return "", fmt.Errorf("failed to delete old key '%s': %w", oldKeyPath, errDelete) } - return finalJson, nil + return string(finalJSON), nil } // FixJSON converts non-standard JSON that uses single quotes for strings into diff --git a/sdk/api/handlers/openai/openai_handlers.go b/sdk/api/handlers/openai/openai_handlers.go index 5991341e..4b4a9833 100644 --- a/sdk/api/handlers/openai/openai_handlers.go +++ b/sdk/api/handlers/openai/openai_handlers.go @@ -191,58 +191,58 @@ func convertCompletionsRequestToChatCompletions(rawJSON []byte) []byte { } // Create chat completions structure - out := `{"model":"","messages":[{"role":"user","content":""}]}` + out := []byte(`{"model":"","messages":[{"role":"user","content":""}]}`) // Set model if model := root.Get("model"); model.Exists() { - out, _ = sjson.Set(out, "model", model.String()) + out, _ = sjson.SetBytes(out, "model", model.String()) } // Set the prompt as user message content - out, _ = sjson.Set(out, "messages.0.content", prompt) + out, _ = sjson.SetBytes(out, "messages.0.content", prompt) // Copy other parameters from completions to chat completions if maxTokens := root.Get("max_tokens"); maxTokens.Exists() { - out, _ = sjson.Set(out, "max_tokens", maxTokens.Int()) + out, _ = sjson.SetBytes(out, "max_tokens", maxTokens.Int()) } if temperature := root.Get("temperature"); temperature.Exists() { - out, _ = sjson.Set(out, "temperature", temperature.Float()) + out, _ = sjson.SetBytes(out, "temperature", temperature.Float()) } if topP := root.Get("top_p"); topP.Exists() { - out, _ = sjson.Set(out, "top_p", topP.Float()) + out, _ = sjson.SetBytes(out, "top_p", topP.Float()) } if frequencyPenalty := root.Get("frequency_penalty"); frequencyPenalty.Exists() { - out, _ = sjson.Set(out, "frequency_penalty", frequencyPenalty.Float()) + out, _ = sjson.SetBytes(out, "frequency_penalty", frequencyPenalty.Float()) } if presencePenalty := root.Get("presence_penalty"); presencePenalty.Exists() { - out, _ = sjson.Set(out, "presence_penalty", presencePenalty.Float()) + out, _ = sjson.SetBytes(out, "presence_penalty", presencePenalty.Float()) } if stop := root.Get("stop"); stop.Exists() { - out, _ = sjson.SetRaw(out, "stop", stop.Raw) + out, _ = sjson.SetRawBytes(out, "stop", []byte(stop.Raw)) } if stream := root.Get("stream"); stream.Exists() { - out, _ = sjson.Set(out, "stream", stream.Bool()) + out, _ = sjson.SetBytes(out, "stream", stream.Bool()) } if logprobs := root.Get("logprobs"); logprobs.Exists() { - out, _ = sjson.Set(out, "logprobs", logprobs.Bool()) + out, _ = sjson.SetBytes(out, "logprobs", logprobs.Bool()) } if topLogprobs := root.Get("top_logprobs"); topLogprobs.Exists() { - out, _ = sjson.Set(out, "top_logprobs", topLogprobs.Int()) + out, _ = sjson.SetBytes(out, "top_logprobs", topLogprobs.Int()) } if echo := root.Get("echo"); echo.Exists() { - out, _ = sjson.Set(out, "echo", echo.Bool()) + out, _ = sjson.SetBytes(out, "echo", echo.Bool()) } - return []byte(out) + return out } // convertChatCompletionsResponseToCompletions converts chat completions API response back to completions format. @@ -257,23 +257,23 @@ func convertChatCompletionsResponseToCompletions(rawJSON []byte) []byte { root := gjson.ParseBytes(rawJSON) // Base completions response structure - out := `{"id":"","object":"text_completion","created":0,"model":"","choices":[]}` + out := []byte(`{"id":"","object":"text_completion","created":0,"model":"","choices":[]}`) // Copy basic fields if id := root.Get("id"); id.Exists() { - out, _ = sjson.Set(out, "id", id.String()) + out, _ = sjson.SetBytes(out, "id", id.String()) } if created := root.Get("created"); created.Exists() { - out, _ = sjson.Set(out, "created", created.Int()) + out, _ = sjson.SetBytes(out, "created", created.Int()) } if model := root.Get("model"); model.Exists() { - out, _ = sjson.Set(out, "model", model.String()) + out, _ = sjson.SetBytes(out, "model", model.String()) } if usage := root.Get("usage"); usage.Exists() { - out, _ = sjson.SetRaw(out, "usage", usage.Raw) + out, _ = sjson.SetRawBytes(out, "usage", []byte(usage.Raw)) } // Convert choices from chat completions to completions format @@ -313,10 +313,10 @@ func convertChatCompletionsResponseToCompletions(rawJSON []byte) []byte { if len(choices) > 0 { choicesJSON, _ := json.Marshal(choices) - out, _ = sjson.SetRaw(out, "choices", string(choicesJSON)) + out, _ = sjson.SetRawBytes(out, "choices", choicesJSON) } - return []byte(out) + return out } // convertChatCompletionsStreamChunkToCompletions converts a streaming chat completions chunk to completions format. @@ -357,19 +357,19 @@ func convertChatCompletionsStreamChunkToCompletions(chunkData []byte) []byte { } // Base completions stream response structure - out := `{"id":"","object":"text_completion","created":0,"model":"","choices":[]}` + out := []byte(`{"id":"","object":"text_completion","created":0,"model":"","choices":[]}`) // Copy basic fields if id := root.Get("id"); id.Exists() { - out, _ = sjson.Set(out, "id", id.String()) + out, _ = sjson.SetBytes(out, "id", id.String()) } if created := root.Get("created"); created.Exists() { - out, _ = sjson.Set(out, "created", created.Int()) + out, _ = sjson.SetBytes(out, "created", created.Int()) } if model := root.Get("model"); model.Exists() { - out, _ = sjson.Set(out, "model", model.String()) + out, _ = sjson.SetBytes(out, "model", model.String()) } // Convert choices from chat completions delta to completions format @@ -408,15 +408,15 @@ func convertChatCompletionsStreamChunkToCompletions(chunkData []byte) []byte { if len(choices) > 0 { choicesJSON, _ := json.Marshal(choices) - out, _ = sjson.SetRaw(out, "choices", string(choicesJSON)) + out, _ = sjson.SetRawBytes(out, "choices", choicesJSON) } // Copy usage if present if usage := root.Get("usage"); usage.Exists() { - out, _ = sjson.SetRaw(out, "usage", usage.Raw) + out, _ = sjson.SetRawBytes(out, "usage", []byte(usage.Raw)) } - return []byte(out) + return out } // handleNonStreamingResponse handles non-streaming chat completion responses diff --git a/sdk/auth/antigravity.go b/sdk/auth/antigravity.go index 6ed31d6d..d52bf1d2 100644 --- a/sdk/auth/antigravity.go +++ b/sdk/auth/antigravity.go @@ -98,6 +98,9 @@ func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, o defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -115,10 +118,11 @@ waitForCallback: break waitForCallback default: } - input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the antigravity callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -132,6 +136,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual case <-timeoutTimer.C: return nil, fmt.Errorf("antigravity: authentication timed out") } diff --git a/sdk/auth/claude.go b/sdk/auth/claude.go index 706763b3..d82a718b 100644 --- a/sdk/auth/claude.go +++ b/sdk/auth/claude.go @@ -124,6 +124,9 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -149,10 +152,11 @@ waitForCallback: return nil, err default: } - input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Claude callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -167,6 +171,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } diff --git a/sdk/auth/codex.go b/sdk/auth/codex.go index 1af36936..269e3d8b 100644 --- a/sdk/auth/codex.go +++ b/sdk/auth/codex.go @@ -127,6 +127,9 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -152,10 +155,11 @@ waitForCallback: return nil, err default: } - input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Codex callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -170,6 +174,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } diff --git a/sdk/auth/iflow.go b/sdk/auth/iflow.go index a695311d..584a3169 100644 --- a/sdk/auth/iflow.go +++ b/sdk/auth/iflow.go @@ -109,6 +109,9 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -128,10 +131,11 @@ waitForCallback: return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err) default: } - input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the iFlow callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -145,6 +149,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } if result.Error != "" { diff --git a/sdk/translator/helpers.go b/sdk/translator/helpers.go index bf8cfbf7..0266b6a8 100644 --- a/sdk/translator/helpers.go +++ b/sdk/translator/helpers.go @@ -13,16 +13,16 @@ func HasResponseTransformerByFormatName(from, to Format) bool { } // TranslateStreamByFormatName converts streaming responses between schemas by their string identifiers. -func TranslateStreamByFormatName(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func TranslateStreamByFormatName(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { return TranslateStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) } // TranslateNonStreamByFormatName converts non-streaming responses between schemas by their string identifiers. -func TranslateNonStreamByFormatName(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +func TranslateNonStreamByFormatName(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { return TranslateNonStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) } // TranslateTokenCountByFormatName converts token counts between schemas by their string identifiers. -func TranslateTokenCountByFormatName(ctx context.Context, from, to Format, count int64, rawJSON []byte) string { +func TranslateTokenCountByFormatName(ctx context.Context, from, to Format, count int64, rawJSON []byte) []byte { return TranslateTokenCount(ctx, from, to, count, rawJSON) } diff --git a/sdk/translator/pipeline.go b/sdk/translator/pipeline.go index 5fa6c66a..16fb0244 100644 --- a/sdk/translator/pipeline.go +++ b/sdk/translator/pipeline.go @@ -16,7 +16,7 @@ type ResponseEnvelope struct { Model string Stream bool Body []byte - Chunks []string + Chunks [][]byte } // RequestMiddleware decorates request translation. @@ -87,7 +87,7 @@ func (p *Pipeline) TranslateResponse(ctx context.Context, from, to Format, resp if input.Stream { input.Chunks = p.registry.TranslateStream(ctx, from, to, input.Model, originalReq, translatedReq, input.Body, param) } else { - input.Body = []byte(p.registry.TranslateNonStream(ctx, from, to, input.Model, originalReq, translatedReq, input.Body, param)) + input.Body = p.registry.TranslateNonStream(ctx, from, to, input.Model, originalReq, translatedReq, input.Body, param) } input.Format = to return input, nil diff --git a/sdk/translator/registry.go b/sdk/translator/registry.go index ace97137..28a5a580 100644 --- a/sdk/translator/registry.go +++ b/sdk/translator/registry.go @@ -66,7 +66,7 @@ func (r *Registry) HasResponseTransformer(from, to Format) bool { } // TranslateStream applies the registered streaming response translator. -func (r *Registry) TranslateStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func (r *Registry) TranslateStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { r.mu.RLock() defer r.mu.RUnlock() @@ -75,11 +75,11 @@ func (r *Registry) TranslateStream(ctx context.Context, from, to Format, model s return fn.Stream(ctx, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) } } - return []string{string(rawJSON)} + return [][]byte{rawJSON} } // TranslateNonStream applies the registered non-stream response translator. -func (r *Registry) TranslateNonStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +func (r *Registry) TranslateNonStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { r.mu.RLock() defer r.mu.RUnlock() @@ -88,11 +88,11 @@ func (r *Registry) TranslateNonStream(ctx context.Context, from, to Format, mode return fn.NonStream(ctx, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) } } - return string(rawJSON) + return rawJSON } -// TranslateNonStream applies the registered non-stream response translator. -func (r *Registry) TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) string { +// TranslateTokenCount applies the registered token count response translator. +func (r *Registry) TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) []byte { r.mu.RLock() defer r.mu.RUnlock() @@ -101,7 +101,7 @@ func (r *Registry) TranslateTokenCount(ctx context.Context, from, to Format, cou return fn.TokenCount(ctx, count) } } - return string(rawJSON) + return rawJSON } var defaultRegistry = NewRegistry() @@ -127,16 +127,16 @@ func HasResponseTransformer(from, to Format) bool { } // TranslateStream is a helper on the default registry. -func TranslateStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string { +func TranslateStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { return defaultRegistry.TranslateStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) } // TranslateNonStream is a helper on the default registry. -func TranslateNonStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string { +func TranslateNonStream(ctx context.Context, from, to Format, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { return defaultRegistry.TranslateNonStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param) } // TranslateTokenCount is a helper on the default registry. -func TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) string { +func TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) []byte { return defaultRegistry.TranslateTokenCount(ctx, from, to, count, rawJSON) } diff --git a/sdk/translator/registry_bytes_test.go b/sdk/translator/registry_bytes_test.go new file mode 100644 index 00000000..014b57f3 --- /dev/null +++ b/sdk/translator/registry_bytes_test.go @@ -0,0 +1,52 @@ +package translator + +import ( + "bytes" + "context" + "testing" +) + +func TestRegistryTranslateStreamReturnsByteChunks(t *testing.T) { + registry := NewRegistry() + registry.Register(FormatOpenAI, FormatGemini, nil, ResponseTransform{ + Stream: func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte { + return [][]byte{append([]byte(nil), rawJSON...)} + }, + }) + + got := registry.TranslateStream(context.Background(), FormatGemini, FormatOpenAI, "model", nil, nil, []byte(`{"chunk":true}`), nil) + if len(got) != 1 { + t.Fatalf("expected 1 chunk, got %d", len(got)) + } + if !bytes.Equal(got[0], []byte(`{"chunk":true}`)) { + t.Fatalf("unexpected chunk: %s", got[0]) + } +} + +func TestRegistryTranslateNonStreamReturnsBytes(t *testing.T) { + registry := NewRegistry() + registry.Register(FormatOpenAI, FormatGemini, nil, ResponseTransform{ + NonStream: func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte { + return append([]byte(nil), rawJSON...) + }, + }) + + got := registry.TranslateNonStream(context.Background(), FormatGemini, FormatOpenAI, "model", nil, nil, []byte(`{"done":true}`), nil) + if !bytes.Equal(got, []byte(`{"done":true}`)) { + t.Fatalf("unexpected payload: %s", got) + } +} + +func TestRegistryTranslateTokenCountReturnsBytes(t *testing.T) { + registry := NewRegistry() + registry.Register(FormatOpenAI, FormatGemini, nil, ResponseTransform{ + TokenCount: func(ctx context.Context, count int64) []byte { + return []byte(`{"totalTokens":7}`) + }, + }) + + got := registry.TranslateTokenCount(context.Background(), FormatGemini, FormatOpenAI, 7, []byte(`{"fallback":true}`)) + if !bytes.Equal(got, []byte(`{"totalTokens":7}`)) { + t.Fatalf("unexpected payload: %s", got) + } +} diff --git a/sdk/translator/types.go b/sdk/translator/types.go index ff69340a..068616b7 100644 --- a/sdk/translator/types.go +++ b/sdk/translator/types.go @@ -10,17 +10,17 @@ type RequestTransform func(model string, rawJSON []byte, stream bool) []byte // ResponseStreamTransform is a function type that converts a streaming response from a source schema to a target schema. // It takes a context, the model name, the raw JSON of the original and converted requests, the raw JSON of the current response chunk, and an optional parameter. -// It returns a slice of strings, where each string is a chunk of the converted streaming response. -type ResponseStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string +// It returns a slice of byte chunks containing the converted streaming response. +type ResponseStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte // ResponseNonStreamTransform is a function type that converts a non-streaming response from a source schema to a target schema. // It takes a context, the model name, the raw JSON of the original and converted requests, the raw JSON of the response, and an optional parameter. -// It returns the converted response as a single string. -type ResponseNonStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string +// It returns the converted response as a single byte slice. +type ResponseNonStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte // ResponseTokenCountTransform is a function type that transforms a token count from a source format to a target format. -// It takes a context and the token count as an int64, and returns the transformed token count as a string. -type ResponseTokenCountTransform func(ctx context.Context, count int64) string +// It takes a context and the token count as an int64, and returns the transformed token count as bytes. +type ResponseTokenCountTransform func(ctx context.Context, count int64) []byte // ResponseTransform is a struct that groups together the functions for transforming streaming and non-streaming responses, // as well as token counts.