mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-23 21:02:38 +00:00
Merge branch 'pr-454'
# Conflicts: # cmd/server/main.go # internal/translator/claude/openai/chat-completions/claude_openai_response.go
This commit is contained in:
@@ -103,6 +103,7 @@ func main() {
|
|||||||
var standalone bool
|
var standalone bool
|
||||||
var noIncognito bool
|
var noIncognito bool
|
||||||
var useIncognito bool
|
var useIncognito bool
|
||||||
|
var localModel bool
|
||||||
|
|
||||||
// Define command-line flags for different operation modes.
|
// Define command-line flags for different operation modes.
|
||||||
flag.BoolVar(&login, "login", false, "Login Google Account")
|
flag.BoolVar(&login, "login", false, "Login Google Account")
|
||||||
@@ -137,6 +138,7 @@ func main() {
|
|||||||
flag.StringVar(&password, "password", "", "")
|
flag.StringVar(&password, "password", "", "")
|
||||||
flag.BoolVar(&tuiMode, "tui", false, "Start with terminal management UI")
|
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(&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() {
|
flag.CommandLine.Usage = func() {
|
||||||
out := flag.CommandLine.Output()
|
out := flag.CommandLine.Output()
|
||||||
@@ -578,11 +580,16 @@ func main() {
|
|||||||
cmd.WaitForCloudDeploy()
|
cmd.WaitForCloudDeploy()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if localModel && (!tuiMode || standalone) {
|
||||||
|
log.Info("Local model mode: using embedded model catalog, remote model updates disabled")
|
||||||
|
}
|
||||||
if tuiMode {
|
if tuiMode {
|
||||||
if standalone {
|
if standalone {
|
||||||
// Standalone mode: start an embedded local server and connect TUI client to it.
|
// Standalone mode: start an embedded local server and connect TUI client to it.
|
||||||
managementasset.StartAutoUpdater(context.Background(), configFilePath)
|
managementasset.StartAutoUpdater(context.Background(), configFilePath)
|
||||||
registry.StartModelsUpdater(context.Background())
|
if !localModel {
|
||||||
|
registry.StartModelsUpdater(context.Background())
|
||||||
|
}
|
||||||
hook := tui.NewLogHook(2000)
|
hook := tui.NewLogHook(2000)
|
||||||
hook.SetFormatter(&logging.LogFormatter{})
|
hook.SetFormatter(&logging.LogFormatter{})
|
||||||
log.AddHook(hook)
|
log.AddHook(hook)
|
||||||
@@ -655,7 +662,9 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
// Start the main proxy service
|
// Start the main proxy service
|
||||||
managementasset.StartAutoUpdater(context.Background(), configFilePath)
|
managementasset.StartAutoUpdater(context.Background(), configFilePath)
|
||||||
registry.StartModelsUpdater(context.Background())
|
if !localModel {
|
||||||
|
registry.StartModelsUpdater(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.AuthDir != "" {
|
if cfg.AuthDir != "" {
|
||||||
kiro.InitializeAndStart(cfg.AuthDir, cfg)
|
kiro.InitializeAndStart(cfg.AuthDir, cfg)
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ func init() {
|
|||||||
sdktr.Register(fOpenAI, fMyProv,
|
sdktr.Register(fOpenAI, fMyProv,
|
||||||
func(model string, raw []byte, stream bool) []byte { return raw },
|
func(model string, raw []byte, stream bool) []byte { return raw },
|
||||||
sdktr.ResponseTransform{
|
sdktr.ResponseTransform{
|
||||||
Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []string {
|
Stream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) [][]byte {
|
||||||
return []string{string(raw)}
|
return [][]byte{raw}
|
||||||
},
|
},
|
||||||
NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) string {
|
NonStream: func(ctx context.Context, model string, originalReq, translatedReq, raw []byte, param *any) []byte {
|
||||||
return string(raw)
|
return raw
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -305,6 +305,9 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
|||||||
defer manualPromptTimer.Stop()
|
defer manualPromptTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manualInputCh <-chan string
|
||||||
|
var manualInputErrCh <-chan error
|
||||||
|
|
||||||
waitForCallback:
|
waitForCallback:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -326,13 +329,14 @@ waitForCallback:
|
|||||||
return nil, err
|
return nil, err
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
input, err := opts.Prompt("Paste the Gemini callback URL (or press Enter to keep waiting): ")
|
manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Gemini callback URL (or press Enter to keep waiting): ")
|
||||||
if err != nil {
|
continue
|
||||||
return nil, err
|
case input := <-manualInputCh:
|
||||||
}
|
manualInputCh = nil
|
||||||
parsed, err := misc.ParseOAuthCallback(input)
|
manualInputErrCh = nil
|
||||||
if err != nil {
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
return nil, err
|
if errParse != nil {
|
||||||
|
return nil, errParse
|
||||||
}
|
}
|
||||||
if parsed == nil {
|
if parsed == nil {
|
||||||
continue
|
continue
|
||||||
@@ -345,6 +349,8 @@ waitForCallback:
|
|||||||
}
|
}
|
||||||
authCode = parsed.Code
|
authCode = parsed.Code
|
||||||
break waitForCallback
|
break waitForCallback
|
||||||
|
case errManual := <-manualInputErrCh:
|
||||||
|
return nil, errManual
|
||||||
case <-timeoutTimer.C:
|
case <-timeoutTimer.C:
|
||||||
return nil, fmt.Errorf("oauth flow timed out")
|
return nil, fmt.Errorf("oauth flow timed out")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,23 @@ type OAuthCallback struct {
|
|||||||
ErrorDescription string
|
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.
|
// ParseOAuthCallback extracts OAuth parameters from a callback URL.
|
||||||
// It returns nil when the input is empty.
|
// It returns nil when the input is empty.
|
||||||
func ParseOAuthCallback(input string) (*OAuthCallback, error) {
|
func ParseOAuthCallback(input string) (*OAuthCallback, error) {
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
reporter.publish(ctx, parseGeminiUsage(wsResp.Body))
|
reporter.publish(ctx, parseGeminiUsage(wsResp.Body))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, ¶m)
|
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
|
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)
|
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
|
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])}
|
||||||
}
|
}
|
||||||
break
|
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)
|
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, ¶m)
|
||||||
for i := range lines {
|
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))
|
reporter.publish(ctx, parseGeminiUsage(event.Payload))
|
||||||
return false
|
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")
|
return cliproxyexecutor.Response{}, fmt.Errorf("wsrelay: totalTokens missing in response")
|
||||||
}
|
}
|
||||||
translated := sdktranslator.TranslateTokenCount(ctx, body.toFormat, opts.SourceFormat, totalTokens, resp.Body)
|
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).
|
// Refresh refreshes the authentication credentials (no-op for AI Studio).
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ attemptLoop:
|
|||||||
reporter.publish(ctx, parseAntigravityUsage(bodyBytes))
|
reporter.publish(ctx, parseAntigravityUsage(bodyBytes))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m)
|
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)
|
reporter.ensurePublished(ctx)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -512,7 +512,7 @@ attemptLoop:
|
|||||||
reporter.publish(ctx, parseAntigravityUsage(resp.Payload))
|
reporter.publish(ctx, parseAntigravityUsage(resp.Payload))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, ¶m)
|
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)
|
reporter.ensurePublished(ctx)
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
@@ -691,31 +691,42 @@ func (e *AntigravityExecutor) convertStreamToNonStream(stream []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
partsJSON, _ := json.Marshal(parts)
|
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 != "" {
|
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 != "" {
|
if finishReason != "" {
|
||||||
responseTemplate, _ = sjson.Set(responseTemplate, "candidates.0.finishReason", finishReason)
|
updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "candidates.0.finishReason", finishReason)
|
||||||
|
responseTemplate = string(updatedTemplate)
|
||||||
}
|
}
|
||||||
if modelVersion != "" {
|
if modelVersion != "" {
|
||||||
responseTemplate, _ = sjson.Set(responseTemplate, "modelVersion", modelVersion)
|
updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "modelVersion", modelVersion)
|
||||||
|
responseTemplate = string(updatedTemplate)
|
||||||
}
|
}
|
||||||
if responseID != "" {
|
if responseID != "" {
|
||||||
responseTemplate, _ = sjson.Set(responseTemplate, "responseId", responseID)
|
updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "responseId", responseID)
|
||||||
|
responseTemplate = string(updatedTemplate)
|
||||||
}
|
}
|
||||||
if usageRaw != "" {
|
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() {
|
} else if !gjson.Get(responseTemplate, "usageMetadata").Exists() {
|
||||||
responseTemplate, _ = sjson.Set(responseTemplate, "usageMetadata.promptTokenCount", 0)
|
updatedTemplate, _ = sjson.SetBytes([]byte(responseTemplate), "usageMetadata.promptTokenCount", 0)
|
||||||
responseTemplate, _ = sjson.Set(responseTemplate, "usageMetadata.candidatesTokenCount", 0)
|
responseTemplate = string(updatedTemplate)
|
||||||
responseTemplate, _ = sjson.Set(responseTemplate, "usageMetadata.totalTokenCount", 0)
|
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 := `{"response":{},"traceId":""}`
|
||||||
output, _ = sjson.SetRaw(output, "response", responseTemplate)
|
updatedOutput, _ := sjson.SetRawBytes([]byte(output), "response", []byte(responseTemplate))
|
||||||
|
output = string(updatedOutput)
|
||||||
if traceID != "" {
|
if traceID != "" {
|
||||||
output, _ = sjson.Set(output, "traceId", traceID)
|
updatedOutput, _ = sjson.SetBytes([]byte(output), "traceId", traceID)
|
||||||
|
output = string(updatedOutput)
|
||||||
}
|
}
|
||||||
return []byte(output)
|
return []byte(output)
|
||||||
}
|
}
|
||||||
@@ -880,12 +891,12 @@ attemptLoop:
|
|||||||
|
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m)
|
||||||
for i := range chunks {
|
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)
|
tail := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, []byte("[DONE]"), ¶m)
|
||||||
for i := range tail {
|
for i := range tail {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(tail[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: tail[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
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 {
|
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
||||||
count := gjson.GetBytes(bodyBytes, "totalTokens").Int()
|
count := gjson.GetBytes(bodyBytes, "totalTokens").Int()
|
||||||
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, bodyBytes)
|
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
|
lastStatus = httpResp.StatusCode
|
||||||
@@ -1265,19 +1276,20 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
|||||||
|
|
||||||
// if useAntigravitySchema {
|
// if useAntigravitySchema {
|
||||||
// systemInstructionPartsResult := gjson.Get(payloadStr, "request.systemInstruction.parts")
|
// systemInstructionPartsResult := gjson.Get(payloadStr, "request.systemInstruction.parts")
|
||||||
// payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.role", "user")
|
// payloadStr, _ = sjson.SetBytes([]byte(payloadStr), "request.systemInstruction.role", "user")
|
||||||
// payloadStr, _ = sjson.Set(payloadStr, "request.systemInstruction.parts.0.text", systemInstruction)
|
// payloadStr, _ = sjson.SetBytes([]byte(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.parts.1.text", fmt.Sprintf("Please ignore following [ignore]%s[/ignore]", systemInstruction))
|
||||||
|
|
||||||
// if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() {
|
// if systemInstructionPartsResult.Exists() && systemInstructionPartsResult.IsArray() {
|
||||||
// for _, partResult := range systemInstructionPartsResult.Array() {
|
// 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") {
|
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 {
|
} else {
|
||||||
payloadStr, _ = sjson.Delete(payloadStr, "request.generationConfig.maxOutputTokens")
|
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 {
|
func geminiToAntigravity(modelName string, payload []byte, projectID string) []byte {
|
||||||
template, _ := sjson.Set(string(payload), "model", modelName)
|
template := payload
|
||||||
template, _ = sjson.Set(template, "userAgent", "antigravity")
|
template, _ = sjson.SetBytes(template, "model", modelName)
|
||||||
|
template, _ = sjson.SetBytes(template, "userAgent", "antigravity")
|
||||||
|
|
||||||
isImageModel := strings.Contains(modelName, "image")
|
isImageModel := strings.Contains(modelName, "image")
|
||||||
|
|
||||||
@@ -1510,28 +1523,28 @@ func geminiToAntigravity(modelName string, payload []byte, projectID string) []b
|
|||||||
} else {
|
} else {
|
||||||
reqType = "agent"
|
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)
|
// Use real project ID from auth if available, otherwise generate random (legacy fallback)
|
||||||
if projectID != "" {
|
if projectID != "" {
|
||||||
template, _ = sjson.Set(template, "project", projectID)
|
template, _ = sjson.SetBytes(template, "project", projectID)
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "project", generateProjectID())
|
template, _ = sjson.SetBytes(template, "project", generateProjectID())
|
||||||
}
|
}
|
||||||
|
|
||||||
if isImageModel {
|
if isImageModel {
|
||||||
template, _ = sjson.Set(template, "requestId", generateImageGenRequestID())
|
template, _ = sjson.SetBytes(template, "requestId", generateImageGenRequestID())
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "requestId", generateRequestID())
|
template, _ = sjson.SetBytes(template, "requestId", generateRequestID())
|
||||||
template, _ = sjson.Set(template, "request.sessionId", generateStableSessionID(payload))
|
template, _ = sjson.SetBytes(template, "request.sessionId", generateStableSessionID(payload))
|
||||||
}
|
}
|
||||||
|
|
||||||
template, _ = sjson.Delete(template, "request.safetySettings")
|
template, _ = sjson.DeleteBytes(template, "request.safetySettings")
|
||||||
if toolConfig := gjson.Get(template, "toolConfig"); toolConfig.Exists() && !gjson.Get(template, "request.toolConfig").Exists() {
|
if toolConfig := gjson.GetBytes(template, "toolConfig"); toolConfig.Exists() && !gjson.GetBytes(template, "request.toolConfig").Exists() {
|
||||||
template, _ = sjson.SetRaw(template, "request.toolConfig", toolConfig.Raw)
|
template, _ = sjson.SetRawBytes(template, "request.toolConfig", []byte(toolConfig.Raw))
|
||||||
template, _ = sjson.Delete(template, "toolConfig")
|
template, _ = sjson.DeleteBytes(template, "toolConfig")
|
||||||
}
|
}
|
||||||
return []byte(template)
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRequestID() string {
|
func generateRequestID() string {
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
data,
|
data,
|
||||||
¶m,
|
¶m,
|
||||||
)
|
)
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
¶m,
|
¶m,
|
||||||
)
|
)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
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)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
count := gjson.GetBytes(data, "input_tokens").Int()
|
count := gjson.GetBytes(data, "input_tokens").Int()
|
||||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
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) {
|
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.
|
// TTL ordering violations with the prompt-caching-scope-2026-01-05 beta.
|
||||||
partJSON := part.Raw
|
partJSON := part.Raw
|
||||||
if !part.Get("cache_control").Exists() {
|
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
|
result += "," + partJSON
|
||||||
}
|
}
|
||||||
@@ -1268,7 +1269,8 @@ func checkSystemInstructionsWithMode(payload []byte, strictMode bool) []byte {
|
|||||||
})
|
})
|
||||||
} else if system.Type == gjson.String && system.String() != "" {
|
} else if system.Type == gjson.String && system.String() != "" {
|
||||||
partJSON := `{"type":"text","cache_control":{"type":"ephemeral"}}`
|
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 += "," + partJSON
|
||||||
}
|
}
|
||||||
result += "]"
|
result += "]"
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
|
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, line, ¶m)
|
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
|
return resp, nil
|
||||||
}
|
}
|
||||||
err = statusErr{code: 408, msg: "stream error: stream disconnected before completion: stream closed before response.completed"}
|
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)
|
reporter.ensurePublished(ctx)
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, ¶m)
|
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
|
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)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, originalPayload, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
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)
|
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))
|
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) {
|
func tokenizerForCodexModel(model string) (tokenizer.Codec, error) {
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
}
|
}
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, ¶m)
|
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
|
return resp, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -592,7 +592,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
line := encodeCodexWebsocketAsSSE(payload)
|
line := encodeCodexWebsocketAsSSE(payload)
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, body, body, line, ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, body, body, line, ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
if !send(cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}) {
|
if !send(cliproxyexecutor.StreamChunk{Payload: chunks[i]}) {
|
||||||
terminateReason = "context_done"
|
terminateReason = "context_done"
|
||||||
terminateErr = ctx.Err()
|
terminateErr = ctx.Err()
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, ¶m)
|
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
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,14 +401,14 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
if bytes.HasPrefix(line, dataTag) {
|
if bytes.HasPrefix(line, dataTag) {
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m)
|
||||||
for i := range segments {
|
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)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, []byte("[DONE]"), ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: segments[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
recordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
@@ -430,12 +430,12 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
var param any
|
var param any
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m)
|
||||||
for i := range segments {
|
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)
|
segments = sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, []byte("[DONE]"), ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(segments[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: segments[i]}
|
||||||
}
|
}
|
||||||
}(httpResp, append([]byte(nil), payload...), attemptModel)
|
}(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 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
|
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
|
lastStatus = resp.StatusCode
|
||||||
lastBody = append([]byte(nil), data...)
|
lastBody = append([]byte(nil), data...)
|
||||||
@@ -811,18 +811,18 @@ func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
|||||||
|
|
||||||
if !hasInlineData {
|
if !hasInlineData {
|
||||||
emptyImageBase64ed, _ := util.CreateWhiteImageBase64(aspectRatioResult.String())
|
emptyImageBase64ed, _ := util.CreateWhiteImageBase64(aspectRatioResult.String())
|
||||||
emptyImagePart := `{"inlineData":{"mime_type":"image/png","data":""}}`
|
emptyImagePart := []byte(`{"inlineData":{"mime_type":"image/png","data":""}}`)
|
||||||
emptyImagePart, _ = sjson.Set(emptyImagePart, "inlineData.data", emptyImageBase64ed)
|
emptyImagePart, _ = sjson.SetBytes(emptyImagePart, "inlineData.data", emptyImageBase64ed)
|
||||||
newPartsJson := `[]`
|
newPartsJson := []byte(`[]`)
|
||||||
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.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.SetRaw(newPartsJson, "-1", emptyImagePart)
|
newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", emptyImagePart)
|
||||||
|
|
||||||
parts := contentArray[0].Get("parts").Array()
|
parts := contentArray[0].Get("parts").Array()
|
||||||
for j := 0; j < len(parts); j++ {
|
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"]`))
|
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.generationConfig.responseModalities", []byte(`["IMAGE", "TEXT"]`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.publish(ctx, parseGeminiUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
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
|
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)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), ¶m)
|
||||||
for i := range lines {
|
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)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
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()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
|
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).
|
// Refresh refreshes the authentication credentials (no-op for Gemini API key).
|
||||||
@@ -527,18 +527,18 @@ func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
|
|||||||
|
|
||||||
if !hasInlineData {
|
if !hasInlineData {
|
||||||
emptyImageBase64ed, _ := util.CreateWhiteImageBase64(aspectRatioResult.String())
|
emptyImageBase64ed, _ := util.CreateWhiteImageBase64(aspectRatioResult.String())
|
||||||
emptyImagePart := `{"inlineData":{"mime_type":"image/png","data":""}}`
|
emptyImagePart := []byte(`{"inlineData":{"mime_type":"image/png","data":""}}`)
|
||||||
emptyImagePart, _ = sjson.Set(emptyImagePart, "inlineData.data", emptyImageBase64ed)
|
emptyImagePart, _ = sjson.SetBytes(emptyImagePart, "inlineData.data", emptyImageBase64ed)
|
||||||
newPartsJson := `[]`
|
newPartsJson := []byte(`[]`)
|
||||||
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.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.SetRaw(newPartsJson, "-1", emptyImagePart)
|
newPartsJson, _ = sjson.SetRawBytes(newPartsJson, "-1", emptyImagePart)
|
||||||
|
|
||||||
parts := contentArray[0].Get("parts").Array()
|
parts := contentArray[0].Get("parts").Array()
|
||||||
for j := 0; j < len(parts); j++ {
|
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"]`))
|
rawJSON, _ = sjson.SetRawBytes(rawJSON, "generationConfig.responseModalities", []byte(`["IMAGE", "TEXT"]`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
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
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,7 +524,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.publish(ctx, parseGeminiUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
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
|
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)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range lines {
|
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)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
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)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range lines {
|
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)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
recordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
@@ -857,7 +857,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
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.
|
// 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)
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
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.
|
// vertexCreds extracts project, location and raw service account JSON from auth metadata.
|
||||||
|
|||||||
@@ -221,13 +221,13 @@ func (e *GitHubCopilotExecutor) Execute(ctx context.Context, auth *cliproxyauth.
|
|||||||
}
|
}
|
||||||
|
|
||||||
var param any
|
var param any
|
||||||
converted := ""
|
var converted []byte
|
||||||
if useResponses && from.String() == "claude" {
|
if useResponses && from.String() == "claude" {
|
||||||
converted = translateGitHubCopilotResponsesNonStreamToClaude(data)
|
converted = translateGitHubCopilotResponsesNonStreamToClaude(data)
|
||||||
} else {
|
} else {
|
||||||
converted = sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
converted = sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, data, ¶m)
|
||||||
}
|
}
|
||||||
resp = cliproxyexecutor.Response{Payload: []byte(converted)}
|
resp = cliproxyexecutor.Response{Payload: converted}
|
||||||
reporter.ensurePublished(ctx)
|
reporter.ensurePublished(ctx)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -374,14 +374,14 @@ func (e *GitHubCopilotExecutor) ExecuteStream(ctx context.Context, auth *cliprox
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunks []string
|
var chunks [][]byte
|
||||||
if useResponses && from.String() == "claude" {
|
if useResponses && from.String() == "claude" {
|
||||||
chunks = translateGitHubCopilotResponsesStreamToClaude(bytes.Clone(line), ¶m)
|
chunks = translateGitHubCopilotResponsesStreamToClaude(bytes.Clone(line), ¶m)
|
||||||
} else {
|
} else {
|
||||||
chunks = sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m)
|
chunks = sdktranslator.TranslateStream(ctx, to, from, req.Model, bytes.Clone(opts.OriginalRequest), body, bytes.Clone(line), ¶m)
|
||||||
}
|
}
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: bytes.Clone(chunks[i])}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,7 +977,7 @@ type githubCopilotResponsesStreamState struct {
|
|||||||
ItemIDToTool map[string]*githubCopilotResponsesStreamToolState
|
ItemIDToTool map[string]*githubCopilotResponsesStreamToolState
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateGitHubCopilotResponsesNonStreamToClaude(data []byte) string {
|
func translateGitHubCopilotResponsesNonStreamToClaude(data []byte) []byte {
|
||||||
root := gjson.ParseBytes(data)
|
root := gjson.ParseBytes(data)
|
||||||
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
out := `{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}`
|
||||||
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
out, _ = sjson.Set(out, "id", root.Get("id").String())
|
||||||
@@ -1067,10 +1067,10 @@ func translateGitHubCopilotResponsesNonStreamToClaude(data []byte) string {
|
|||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "stop_reason", "end_turn")
|
out, _ = sjson.Set(out, "stop_reason", "end_turn")
|
||||||
}
|
}
|
||||||
return out
|
return []byte(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []string {
|
func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &githubCopilotResponsesStreamState{
|
*param = &githubCopilotResponsesStreamState{
|
||||||
TextBlockIndex: -1,
|
TextBlockIndex: -1,
|
||||||
@@ -1092,7 +1092,10 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
}
|
}
|
||||||
|
|
||||||
event := gjson.GetBytes(payload, "type").String()
|
event := gjson.GetBytes(payload, "type").String()
|
||||||
results := make([]string, 0, 4)
|
results := make([][]byte, 0, 4)
|
||||||
|
appendResult := func(chunk string) {
|
||||||
|
results = append(results, []byte(chunk))
|
||||||
|
}
|
||||||
ensureMessageStart := func() {
|
ensureMessageStart := func() {
|
||||||
if state.MessageStarted {
|
if state.MessageStarted {
|
||||||
return
|
return
|
||||||
@@ -1100,7 +1103,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
messageStart := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`
|
messageStart := `{"type":"message_start","message":{"id":"","type":"message","role":"assistant","model":"","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}`
|
||||||
messageStart, _ = sjson.Set(messageStart, "message.id", gjson.GetBytes(payload, "response.id").String())
|
messageStart, _ = sjson.Set(messageStart, "message.id", gjson.GetBytes(payload, "response.id").String())
|
||||||
messageStart, _ = sjson.Set(messageStart, "message.model", gjson.GetBytes(payload, "response.model").String())
|
messageStart, _ = sjson.Set(messageStart, "message.model", gjson.GetBytes(payload, "response.model").String())
|
||||||
results = append(results, "event: message_start\ndata: "+messageStart+"\n\n")
|
appendResult("event: message_start\ndata: " + messageStart + "\n\n")
|
||||||
state.MessageStarted = true
|
state.MessageStarted = true
|
||||||
}
|
}
|
||||||
startTextBlockIfNeeded := func() {
|
startTextBlockIfNeeded := func() {
|
||||||
@@ -1113,7 +1116,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
}
|
}
|
||||||
contentBlockStart := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
contentBlockStart := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
||||||
contentBlockStart, _ = sjson.Set(contentBlockStart, "index", state.TextBlockIndex)
|
contentBlockStart, _ = sjson.Set(contentBlockStart, "index", state.TextBlockIndex)
|
||||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStart+"\n\n")
|
appendResult("event: content_block_start\ndata: " + contentBlockStart + "\n\n")
|
||||||
state.TextBlockStarted = true
|
state.TextBlockStarted = true
|
||||||
}
|
}
|
||||||
stopTextBlockIfNeeded := func() {
|
stopTextBlockIfNeeded := func() {
|
||||||
@@ -1122,7 +1125,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
}
|
}
|
||||||
contentBlockStop := `{"type":"content_block_stop","index":0}`
|
contentBlockStop := `{"type":"content_block_stop","index":0}`
|
||||||
contentBlockStop, _ = sjson.Set(contentBlockStop, "index", state.TextBlockIndex)
|
contentBlockStop, _ = sjson.Set(contentBlockStop, "index", state.TextBlockIndex)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStop+"\n\n")
|
appendResult("event: content_block_stop\ndata: " + contentBlockStop + "\n\n")
|
||||||
state.TextBlockStarted = false
|
state.TextBlockStarted = false
|
||||||
state.TextBlockIndex = -1
|
state.TextBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -1152,7 +1155,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
contentDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
contentDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
||||||
contentDelta, _ = sjson.Set(contentDelta, "index", state.TextBlockIndex)
|
contentDelta, _ = sjson.Set(contentDelta, "index", state.TextBlockIndex)
|
||||||
contentDelta, _ = sjson.Set(contentDelta, "delta.text", delta)
|
contentDelta, _ = sjson.Set(contentDelta, "delta.text", delta)
|
||||||
results = append(results, "event: content_block_delta\ndata: "+contentDelta+"\n\n")
|
appendResult("event: content_block_delta\ndata: " + contentDelta + "\n\n")
|
||||||
}
|
}
|
||||||
case "response.reasoning_summary_part.added":
|
case "response.reasoning_summary_part.added":
|
||||||
ensureMessageStart()
|
ensureMessageStart()
|
||||||
@@ -1161,7 +1164,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
state.NextContentIndex++
|
state.NextContentIndex++
|
||||||
thinkingStart := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
thinkingStart := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
||||||
thinkingStart, _ = sjson.Set(thinkingStart, "index", state.ReasoningIndex)
|
thinkingStart, _ = sjson.Set(thinkingStart, "index", state.ReasoningIndex)
|
||||||
results = append(results, "event: content_block_start\ndata: "+thinkingStart+"\n\n")
|
appendResult("event: content_block_start\ndata: " + thinkingStart + "\n\n")
|
||||||
case "response.reasoning_summary_text.delta":
|
case "response.reasoning_summary_text.delta":
|
||||||
if state.ReasoningActive {
|
if state.ReasoningActive {
|
||||||
delta := gjson.GetBytes(payload, "delta").String()
|
delta := gjson.GetBytes(payload, "delta").String()
|
||||||
@@ -1169,14 +1172,14 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
thinkingDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
thinkingDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
||||||
thinkingDelta, _ = sjson.Set(thinkingDelta, "index", state.ReasoningIndex)
|
thinkingDelta, _ = sjson.Set(thinkingDelta, "index", state.ReasoningIndex)
|
||||||
thinkingDelta, _ = sjson.Set(thinkingDelta, "delta.thinking", delta)
|
thinkingDelta, _ = sjson.Set(thinkingDelta, "delta.thinking", delta)
|
||||||
results = append(results, "event: content_block_delta\ndata: "+thinkingDelta+"\n\n")
|
appendResult("event: content_block_delta\ndata: " + thinkingDelta + "\n\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "response.reasoning_summary_part.done":
|
case "response.reasoning_summary_part.done":
|
||||||
if state.ReasoningActive {
|
if state.ReasoningActive {
|
||||||
thinkingStop := `{"type":"content_block_stop","index":0}`
|
thinkingStop := `{"type":"content_block_stop","index":0}`
|
||||||
thinkingStop, _ = sjson.Set(thinkingStop, "index", state.ReasoningIndex)
|
thinkingStop, _ = sjson.Set(thinkingStop, "index", state.ReasoningIndex)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+thinkingStop+"\n\n")
|
appendResult("event: content_block_stop\ndata: " + thinkingStop + "\n\n")
|
||||||
state.ReasoningActive = false
|
state.ReasoningActive = false
|
||||||
}
|
}
|
||||||
case "response.output_item.added":
|
case "response.output_item.added":
|
||||||
@@ -1204,7 +1207,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
contentBlockStart, _ = sjson.Set(contentBlockStart, "index", tool.Index)
|
contentBlockStart, _ = sjson.Set(contentBlockStart, "index", tool.Index)
|
||||||
contentBlockStart, _ = sjson.Set(contentBlockStart, "content_block.id", tool.ID)
|
contentBlockStart, _ = sjson.Set(contentBlockStart, "content_block.id", tool.ID)
|
||||||
contentBlockStart, _ = sjson.Set(contentBlockStart, "content_block.name", tool.Name)
|
contentBlockStart, _ = sjson.Set(contentBlockStart, "content_block.name", tool.Name)
|
||||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStart+"\n\n")
|
appendResult("event: content_block_start\ndata: " + contentBlockStart + "\n\n")
|
||||||
case "response.output_item.delta":
|
case "response.output_item.delta":
|
||||||
item := gjson.GetBytes(payload, "item")
|
item := gjson.GetBytes(payload, "item")
|
||||||
if item.Get("type").String() != "function_call" {
|
if item.Get("type").String() != "function_call" {
|
||||||
@@ -1224,7 +1227,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
inputDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
inputDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||||
inputDelta, _ = sjson.Set(inputDelta, "index", tool.Index)
|
inputDelta, _ = sjson.Set(inputDelta, "index", tool.Index)
|
||||||
inputDelta, _ = sjson.Set(inputDelta, "delta.partial_json", partial)
|
inputDelta, _ = sjson.Set(inputDelta, "delta.partial_json", partial)
|
||||||
results = append(results, "event: content_block_delta\ndata: "+inputDelta+"\n\n")
|
appendResult("event: content_block_delta\ndata: " + inputDelta + "\n\n")
|
||||||
case "response.function_call_arguments.delta":
|
case "response.function_call_arguments.delta":
|
||||||
// Copilot sends tool call arguments via this event type (not response.output_item.delta).
|
// Copilot sends tool call arguments via this event type (not response.output_item.delta).
|
||||||
// Data format: {"delta":"...", "item_id":"...", "output_index":N, ...}
|
// Data format: {"delta":"...", "item_id":"...", "output_index":N, ...}
|
||||||
@@ -1241,7 +1244,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
inputDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
inputDelta := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
||||||
inputDelta, _ = sjson.Set(inputDelta, "index", tool.Index)
|
inputDelta, _ = sjson.Set(inputDelta, "index", tool.Index)
|
||||||
inputDelta, _ = sjson.Set(inputDelta, "delta.partial_json", partial)
|
inputDelta, _ = sjson.Set(inputDelta, "delta.partial_json", partial)
|
||||||
results = append(results, "event: content_block_delta\ndata: "+inputDelta+"\n\n")
|
appendResult("event: content_block_delta\ndata: " + inputDelta + "\n\n")
|
||||||
case "response.output_item.done":
|
case "response.output_item.done":
|
||||||
if gjson.GetBytes(payload, "item.type").String() != "function_call" {
|
if gjson.GetBytes(payload, "item.type").String() != "function_call" {
|
||||||
break
|
break
|
||||||
@@ -1252,7 +1255,7 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
}
|
}
|
||||||
contentBlockStop := `{"type":"content_block_stop","index":0}`
|
contentBlockStop := `{"type":"content_block_stop","index":0}`
|
||||||
contentBlockStop, _ = sjson.Set(contentBlockStop, "index", tool.Index)
|
contentBlockStop, _ = sjson.Set(contentBlockStop, "index", tool.Index)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStop+"\n\n")
|
appendResult("event: content_block_stop\ndata: " + contentBlockStop + "\n\n")
|
||||||
case "response.completed":
|
case "response.completed":
|
||||||
ensureMessageStart()
|
ensureMessageStart()
|
||||||
stopTextBlockIfNeeded()
|
stopTextBlockIfNeeded()
|
||||||
@@ -1276,8 +1279,8 @@ func translateGitHubCopilotResponsesStreamToClaude(line []byte, param *any) []st
|
|||||||
if cachedTokens > 0 {
|
if cachedTokens > 0 {
|
||||||
messageDelta, _ = sjson.Set(messageDelta, "usage.cache_read_input_tokens", cachedTokens)
|
messageDelta, _ = sjson.Set(messageDelta, "usage.cache_read_input_tokens", cachedTokens)
|
||||||
}
|
}
|
||||||
results = append(results, "event: message_delta\ndata: "+messageDelta+"\n\n")
|
appendResult("event: message_delta\ndata: " + messageDelta + "\n\n")
|
||||||
results = append(results, "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
|
appendResult("event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n")
|
||||||
state.MessageStopSent = true
|
state.MessageStopSent = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
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
|
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)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
@@ -315,7 +315,7 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
usageJSON := buildOpenAIUsageJSON(count)
|
usageJSON := buildOpenAIUsageJSON(count)
|
||||||
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
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.
|
// Refresh refreshes OAuth tokens or cookie-based API keys and updates the stored API key.
|
||||||
|
|||||||
@@ -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
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
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
|
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)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
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)
|
doneChunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range doneChunks {
|
for i := range doneChunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(doneChunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
recordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ var endpointAliases = map[string]string{
|
|||||||
"cli": "amazonq",
|
"cli": "amazonq",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enqueueTranslatedSSE(out chan<- cliproxyexecutor.StreamChunk, chunk []byte) {
|
||||||
|
if len(chunk) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out <- cliproxyexecutor.StreamChunk{Payload: append(bytes.Clone(chunk), '\n', '\n')}
|
||||||
|
}
|
||||||
|
|
||||||
// retryConfig holds configuration for socket retry logic.
|
// retryConfig holds configuration for socket retry logic.
|
||||||
// Based on kiro2Api Python implementation patterns.
|
// Based on kiro2Api Python implementation patterns.
|
||||||
type retryConfig struct {
|
type retryConfig struct {
|
||||||
@@ -2573,9 +2580,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", currentToolUse.ToolUseID, currentToolUse.Name)
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", currentToolUse.ToolUseID, currentToolUse.Name)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send tool input as delta
|
// Send tool input as delta
|
||||||
@@ -2583,18 +2588,14 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputBytes), contentBlockIndex)
|
inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputBytes), contentBlockIndex)
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close block
|
// Close block
|
||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasToolUses = true
|
hasToolUses = true
|
||||||
@@ -2664,9 +2665,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
msgStart := kiroclaude.BuildClaudeMessageStartEvent(model, totalUsage.InputTokens)
|
msgStart := kiroclaude.BuildClaudeMessageStartEvent(model, totalUsage.InputTokens)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
messageStartSent = true
|
messageStartSent = true
|
||||||
}
|
}
|
||||||
@@ -2916,9 +2915,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
pingEvent := kiroclaude.BuildClaudePingEventWithUsage(totalUsage.InputTokens, currentOutputTokens)
|
pingEvent := kiroclaude.BuildClaudePingEventWithUsage(totalUsage.InputTokens, currentOutputTokens)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, pingEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, pingEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastReportedOutputTokens = currentOutputTokens
|
lastReportedOutputTokens = currentOutputTokens
|
||||||
@@ -2939,17 +2936,13 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
claudeEvent := kiroclaude.BuildClaudeStreamEvent(processText, contentBlockIndex)
|
claudeEvent := kiroclaude.BuildClaudeStreamEvent(processText, contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -2978,18 +2971,14 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send thinking delta
|
// Send thinking delta
|
||||||
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkingText, thinkingBlockIndex)
|
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkingText, thinkingBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
accumulatedThinkingContent.WriteString(thinkingText)
|
accumulatedThinkingContent.WriteString(thinkingText)
|
||||||
}
|
}
|
||||||
@@ -2998,9 +2987,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex)
|
blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isThinkingBlockOpen = false
|
isThinkingBlockOpen = false
|
||||||
}
|
}
|
||||||
@@ -3029,17 +3016,13 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(processContent, thinkingBlockIndex)
|
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(processContent, thinkingBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
accumulatedThinkingContent.WriteString(processContent)
|
accumulatedThinkingContent.WriteString(processContent)
|
||||||
}
|
}
|
||||||
@@ -3058,9 +3041,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex)
|
blockStop := kiroclaude.BuildClaudeThinkingBlockStopEvent(thinkingBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isThinkingBlockOpen = false
|
isThinkingBlockOpen = false
|
||||||
}
|
}
|
||||||
@@ -3071,18 +3052,14 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Send text delta
|
// Send text delta
|
||||||
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textBefore, contentBlockIndex)
|
claudeEvent := kiroclaude.BuildClaudeStreamEvent(textBefore, contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Close text block before entering thinking
|
// Close text block before entering thinking
|
||||||
@@ -3090,9 +3067,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isTextBlockOpen = false
|
isTextBlockOpen = false
|
||||||
}
|
}
|
||||||
@@ -3120,17 +3095,13 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "text", "", "")
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
claudeEvent := kiroclaude.BuildClaudeStreamEvent(processContent, contentBlockIndex)
|
claudeEvent := kiroclaude.BuildClaudeStreamEvent(processContent, contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, claudeEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3158,9 +3129,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isTextBlockOpen = false
|
isTextBlockOpen = false
|
||||||
}
|
}
|
||||||
@@ -3171,9 +3140,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", toolUseID, toolName)
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", toolUseID, toolName)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send input_json_delta with the tool input
|
// Send input_json_delta with the tool input
|
||||||
@@ -3186,9 +3153,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex)
|
inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex)
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3197,9 +3162,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3239,9 +3202,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isTextBlockOpen = false
|
isTextBlockOpen = false
|
||||||
}
|
}
|
||||||
@@ -3254,9 +3215,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(thinkingBlockIndex, "thinking", "", "")
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3264,9 +3223,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkingText, thinkingBlockIndex)
|
thinkingEvent := kiroclaude.BuildClaudeThinkingDeltaEvent(thinkingText, thinkingBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, thinkingEvent, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accumulate for token counting
|
// Accumulate for token counting
|
||||||
@@ -3298,9 +3255,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isTextBlockOpen = false
|
isTextBlockOpen = false
|
||||||
}
|
}
|
||||||
@@ -3310,9 +3265,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", tu.ToolUseID, tu.Name)
|
blockStart := kiroclaude.BuildClaudeContentBlockStartEvent(contentBlockIndex, "tool_use", tu.ToolUseID, tu.Name)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStart, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if tu.Input != nil {
|
if tu.Input != nil {
|
||||||
@@ -3323,9 +3276,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex)
|
inputDelta := kiroclaude.BuildClaudeInputJsonDeltaEvent(string(inputJSON), contentBlockIndex)
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, inputDelta, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3333,9 +3284,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3522,9 +3471,7 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
blockStop := kiroclaude.BuildClaudeContentBlockStopEvent(contentBlockIndex)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, blockStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3609,18 +3556,14 @@ func (e *KiroExecutor) streamToChannel(ctx context.Context, body io.Reader, out
|
|||||||
msgDelta := kiroclaude.BuildClaudeMessageDeltaEvent(stopReason, totalUsage)
|
msgDelta := kiroclaude.BuildClaudeMessageDeltaEvent(stopReason, totalUsage)
|
||||||
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgDelta, &translatorParam)
|
sseData := sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgDelta, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message_stop event separately
|
// Send message_stop event separately
|
||||||
msgStop := kiroclaude.BuildClaudeMessageStopOnlyEvent()
|
msgStop := kiroclaude.BuildClaudeMessageStopOnlyEvent()
|
||||||
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStop, &translatorParam)
|
sseData = sdktranslator.TranslateStream(ctx, sdktranslator.FromString("kiro"), targetFormat, model, originalReq, claudeBody, msgStop, &translatorParam)
|
||||||
for _, chunk := range sseData {
|
for _, chunk := range sseData {
|
||||||
if chunk != "" {
|
enqueueTranslatedSSE(out, chunk)
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunk + "\n\n")}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// reporter.publish is called via defer
|
// reporter.publish is called via defer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
// Translate response back to source format when needed
|
// Translate response back to source format when needed
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, ¶m)
|
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
|
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.
|
// 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)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
@@ -330,7 +330,7 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau
|
|||||||
|
|
||||||
usageJSON := buildOpenAIUsageJSON(count)
|
usageJSON := buildOpenAIUsageJSON(count)
|
||||||
translatedUsage := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
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.
|
// Refresh is a no-op for API-key based compatibility providers.
|
||||||
|
|||||||
@@ -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
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
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
|
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)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
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)
|
doneChunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), ¶m)
|
||||||
for i := range doneChunks {
|
for i := range doneChunks {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: []byte(doneChunks[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
recordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
@@ -461,7 +461,7 @@ func (e *QwenExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
|
|
||||||
usageJSON := buildOpenAIUsageJSON(count)
|
usageJSON := buildOpenAIUsageJSON(count)
|
||||||
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
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) {
|
func (e *QwenExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {
|
||||||
|
|||||||
@@ -40,33 +40,33 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
systemInstructionJSON := ""
|
var systemInstructionJSON []byte
|
||||||
hasSystemInstruction := false
|
hasSystemInstruction := false
|
||||||
systemResult := gjson.GetBytes(rawJSON, "system")
|
systemResult := gjson.GetBytes(rawJSON, "system")
|
||||||
if systemResult.IsArray() {
|
if systemResult.IsArray() {
|
||||||
systemResults := systemResult.Array()
|
systemResults := systemResult.Array()
|
||||||
systemInstructionJSON = `{"role":"user","parts":[]}`
|
systemInstructionJSON = []byte(`{"role":"user","parts":[]}`)
|
||||||
for i := 0; i < len(systemResults); i++ {
|
for i := 0; i < len(systemResults); i++ {
|
||||||
systemPromptResult := systemResults[i]
|
systemPromptResult := systemResults[i]
|
||||||
systemTypePromptResult := systemPromptResult.Get("type")
|
systemTypePromptResult := systemPromptResult.Get("type")
|
||||||
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
if systemTypePromptResult.Type == gjson.String && systemTypePromptResult.String() == "text" {
|
||||||
systemPrompt := systemPromptResult.Get("text").String()
|
systemPrompt := systemPromptResult.Get("text").String()
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
if systemPrompt != "" {
|
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
|
hasSystemInstruction = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if systemResult.Type == gjson.String {
|
} else if systemResult.Type == gjson.String {
|
||||||
systemInstructionJSON = `{"role":"user","parts":[{"text":""}]}`
|
systemInstructionJSON = []byte(`{"role":"user","parts":[{"text":""}]}`)
|
||||||
systemInstructionJSON, _ = sjson.Set(systemInstructionJSON, "parts.0.text", systemResult.String())
|
systemInstructionJSON, _ = sjson.SetBytes(systemInstructionJSON, "parts.0.text", systemResult.String())
|
||||||
hasSystemInstruction = true
|
hasSystemInstruction = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// contents
|
// contents
|
||||||
contentsJSON := "[]"
|
contentsJSON := []byte(`[]`)
|
||||||
hasContents := false
|
hasContents := false
|
||||||
|
|
||||||
// tool_use_id → tool_name lookup, populated incrementally during the main loop.
|
// 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" {
|
if role == "assistant" {
|
||||||
role = "model"
|
role = "model"
|
||||||
}
|
}
|
||||||
clientContentJSON := `{"role":"","parts":[]}`
|
clientContentJSON := []byte(`{"role":"","parts":[]}`)
|
||||||
clientContentJSON, _ = sjson.Set(clientContentJSON, "role", role)
|
clientContentJSON, _ = sjson.SetBytes(clientContentJSON, "role", role)
|
||||||
contentsResult := messageResult.Get("content")
|
contentsResult := messageResult.Get("content")
|
||||||
if contentsResult.IsArray() {
|
if contentsResult.IsArray() {
|
||||||
contentResults := contentsResult.Array()
|
contentResults := contentsResult.Array()
|
||||||
@@ -148,15 +148,15 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Valid signature, send as thought block
|
// Valid signature, send as thought block
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "thought", true)
|
partJSON, _ = sjson.SetBytes(partJSON, "thought", true)
|
||||||
if thinkingText != "" {
|
if thinkingText != "" {
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", thinkingText)
|
partJSON, _ = sjson.SetBytes(partJSON, "text", thinkingText)
|
||||||
}
|
}
|
||||||
if signature != "" {
|
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" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||||
prompt := contentResult.Get("text").String()
|
prompt := contentResult.Get("text").String()
|
||||||
// Skip empty text parts to avoid Gemini API error:
|
// Skip empty text parts to avoid Gemini API error:
|
||||||
@@ -164,9 +164,9 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
if prompt == "" {
|
if prompt == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", prompt)
|
partJSON, _ = sjson.SetBytes(partJSON, "text", prompt)
|
||||||
clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON)
|
clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON)
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_use" {
|
||||||
// NOTE: Do NOT inject dummy thinking blocks here.
|
// NOTE: Do NOT inject dummy thinking blocks here.
|
||||||
// Antigravity API validates signatures, so dummy values are rejected.
|
// Antigravity API validates signatures, so dummy values are rejected.
|
||||||
@@ -192,25 +192,25 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
|
|
||||||
if argsRaw != "" {
|
if argsRaw != "" {
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
|
|
||||||
// Use skip_thought_signature_validator for tool calls without valid thinking signature
|
// Use skip_thought_signature_validator for tool calls without valid thinking signature
|
||||||
// This is the approach used in opencode-google-antigravity-auth for Gemini
|
// This is the approach used in opencode-google-antigravity-auth for Gemini
|
||||||
// and also works for Claude through Antigravity API
|
// and also works for Claude through Antigravity API
|
||||||
const skipSentinel = "skip_thought_signature_validator"
|
const skipSentinel = "skip_thought_signature_validator"
|
||||||
if cache.HasValidSignature(modelName, currentMessageThinkingSignature) {
|
if cache.HasValidSignature(modelName, currentMessageThinkingSignature) {
|
||||||
partJSON, _ = sjson.Set(partJSON, "thoughtSignature", currentMessageThinkingSignature)
|
partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", currentMessageThinkingSignature)
|
||||||
} else {
|
} else {
|
||||||
// No valid signature - use skip sentinel to bypass validation
|
// No valid signature - use skip sentinel to bypass validation
|
||||||
partJSON, _ = sjson.Set(partJSON, "thoughtSignature", skipSentinel)
|
partJSON, _ = sjson.SetBytes(partJSON, "thoughtSignature", skipSentinel)
|
||||||
}
|
}
|
||||||
|
|
||||||
if functionID != "" {
|
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.SetBytes(partJSON, "functionCall.name", functionName)
|
||||||
partJSON, _ = sjson.SetRaw(partJSON, "functionCall.args", argsRaw)
|
partJSON, _ = sjson.SetRawBytes(partJSON, "functionCall.args", []byte(argsRaw))
|
||||||
clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON)
|
clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON)
|
||||||
}
|
}
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "tool_result" {
|
||||||
toolCallID := contentResult.Get("tool_use_id").String()
|
toolCallID := contentResult.Get("tool_use_id").String()
|
||||||
@@ -231,108 +231,108 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
}
|
}
|
||||||
functionResponseResult := contentResult.Get("content")
|
functionResponseResult := contentResult.Get("content")
|
||||||
|
|
||||||
functionResponseJSON := `{}`
|
functionResponseJSON := []byte(`{}`)
|
||||||
functionResponseJSON, _ = sjson.Set(functionResponseJSON, "id", toolCallID)
|
functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "id", toolCallID)
|
||||||
functionResponseJSON, _ = sjson.Set(functionResponseJSON, "name", funcName)
|
functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "name", funcName)
|
||||||
|
|
||||||
responseData := ""
|
responseData := ""
|
||||||
if functionResponseResult.Type == gjson.String {
|
if functionResponseResult.Type == gjson.String {
|
||||||
responseData = functionResponseResult.String()
|
responseData = functionResponseResult.String()
|
||||||
functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", responseData)
|
functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", responseData)
|
||||||
} else if functionResponseResult.IsArray() {
|
} else if functionResponseResult.IsArray() {
|
||||||
frResults := functionResponseResult.Array()
|
frResults := functionResponseResult.Array()
|
||||||
nonImageCount := 0
|
nonImageCount := 0
|
||||||
lastNonImageRaw := ""
|
lastNonImageRaw := ""
|
||||||
filteredJSON := "[]"
|
filteredJSON := []byte(`[]`)
|
||||||
imagePartsJSON := "[]"
|
imagePartsJSON := []byte(`[]`)
|
||||||
for _, fr := range frResults {
|
for _, fr := range frResults {
|
||||||
if fr.Get("type").String() == "image" && fr.Get("source.type").String() == "base64" {
|
if fr.Get("type").String() == "image" && fr.Get("source.type").String() == "base64" {
|
||||||
inlineDataJSON := `{}`
|
inlineDataJSON := []byte(`{}`)
|
||||||
if mimeType := fr.Get("source.media_type").String(); mimeType != "" {
|
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 != "" {
|
if data := fr.Get("source.data").String(); data != "" {
|
||||||
inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)
|
inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "data", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePartJSON := `{}`
|
imagePartJSON := []byte(`{}`)
|
||||||
imagePartJSON, _ = sjson.SetRaw(imagePartJSON, "inlineData", inlineDataJSON)
|
imagePartJSON, _ = sjson.SetRawBytes(imagePartJSON, "inlineData", inlineDataJSON)
|
||||||
imagePartsJSON, _ = sjson.SetRaw(imagePartsJSON, "-1", imagePartJSON)
|
imagePartsJSON, _ = sjson.SetRawBytes(imagePartsJSON, "-1", imagePartJSON)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
nonImageCount++
|
nonImageCount++
|
||||||
lastNonImageRaw = fr.Raw
|
lastNonImageRaw = fr.Raw
|
||||||
filteredJSON, _ = sjson.SetRaw(filteredJSON, "-1", fr.Raw)
|
filteredJSON, _ = sjson.SetRawBytes(filteredJSON, "-1", []byte(fr.Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
if nonImageCount == 1 {
|
if nonImageCount == 1 {
|
||||||
functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", lastNonImageRaw)
|
functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", []byte(lastNonImageRaw))
|
||||||
} else if nonImageCount > 1 {
|
} else if nonImageCount > 1 {
|
||||||
functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", filteredJSON)
|
functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", filteredJSON)
|
||||||
} else {
|
} else {
|
||||||
functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "")
|
functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place image data inside functionResponse.parts as inlineData
|
// Place image data inside functionResponse.parts as inlineData
|
||||||
// instead of as sibling parts in the outer content, to avoid
|
// instead of as sibling parts in the outer content, to avoid
|
||||||
// base64 data bloating the text context.
|
// base64 data bloating the text context.
|
||||||
if gjson.Get(imagePartsJSON, "#").Int() > 0 {
|
if gjson.GetBytes(imagePartsJSON, "#").Int() > 0 {
|
||||||
functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "parts", imagePartsJSON)
|
functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "parts", imagePartsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if functionResponseResult.IsObject() {
|
} else if functionResponseResult.IsObject() {
|
||||||
if functionResponseResult.Get("type").String() == "image" && functionResponseResult.Get("source.type").String() == "base64" {
|
if functionResponseResult.Get("type").String() == "image" && functionResponseResult.Get("source.type").String() == "base64" {
|
||||||
inlineDataJSON := `{}`
|
inlineDataJSON := []byte(`{}`)
|
||||||
if mimeType := functionResponseResult.Get("source.media_type").String(); mimeType != "" {
|
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 != "" {
|
if data := functionResponseResult.Get("source.data").String(); data != "" {
|
||||||
inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)
|
inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "data", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePartJSON := `{}`
|
imagePartJSON := []byte(`{}`)
|
||||||
imagePartJSON, _ = sjson.SetRaw(imagePartJSON, "inlineData", inlineDataJSON)
|
imagePartJSON, _ = sjson.SetRawBytes(imagePartJSON, "inlineData", inlineDataJSON)
|
||||||
imagePartsJSON := "[]"
|
imagePartsJSON := []byte(`[]`)
|
||||||
imagePartsJSON, _ = sjson.SetRaw(imagePartsJSON, "-1", imagePartJSON)
|
imagePartsJSON, _ = sjson.SetRawBytes(imagePartsJSON, "-1", imagePartJSON)
|
||||||
functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "parts", imagePartsJSON)
|
functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "parts", imagePartsJSON)
|
||||||
functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "")
|
functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", "")
|
||||||
} else {
|
} else {
|
||||||
functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
|
functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", []byte(functionResponseResult.Raw))
|
||||||
}
|
}
|
||||||
} else if functionResponseResult.Raw != "" {
|
} else if functionResponseResult.Raw != "" {
|
||||||
functionResponseJSON, _ = sjson.SetRaw(functionResponseJSON, "response.result", functionResponseResult.Raw)
|
functionResponseJSON, _ = sjson.SetRawBytes(functionResponseJSON, "response.result", []byte(functionResponseResult.Raw))
|
||||||
} else {
|
} else {
|
||||||
// Content field is missing entirely — .Raw is empty which
|
// Content field is missing entirely — .Raw is empty which
|
||||||
// causes sjson.SetRaw to produce invalid JSON (e.g. "result":}).
|
// causes sjson.SetRaw to produce invalid JSON (e.g. "result":}).
|
||||||
functionResponseJSON, _ = sjson.Set(functionResponseJSON, "response.result", "")
|
functionResponseJSON, _ = sjson.SetBytes(functionResponseJSON, "response.result", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
partJSON, _ = sjson.SetRaw(partJSON, "functionResponse", functionResponseJSON)
|
partJSON, _ = sjson.SetRawBytes(partJSON, "functionResponse", functionResponseJSON)
|
||||||
clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON)
|
clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON)
|
||||||
}
|
}
|
||||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "image" {
|
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "image" {
|
||||||
sourceResult := contentResult.Get("source")
|
sourceResult := contentResult.Get("source")
|
||||||
if sourceResult.Get("type").String() == "base64" {
|
if sourceResult.Get("type").String() == "base64" {
|
||||||
inlineDataJSON := `{}`
|
inlineDataJSON := []byte(`{}`)
|
||||||
if mimeType := sourceResult.Get("media_type").String(); mimeType != "" {
|
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 != "" {
|
if data := sourceResult.Get("data").String(); data != "" {
|
||||||
inlineDataJSON, _ = sjson.Set(inlineDataJSON, "data", data)
|
inlineDataJSON, _ = sjson.SetBytes(inlineDataJSON, "data", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
partJSON, _ = sjson.SetRaw(partJSON, "inlineData", inlineDataJSON)
|
partJSON, _ = sjson.SetRawBytes(partJSON, "inlineData", inlineDataJSON)
|
||||||
clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON)
|
clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reorder parts for 'model' role to ensure thinking block is first
|
// Reorder parts for 'model' role to ensure thinking block is first
|
||||||
if role == "model" {
|
if role == "model" {
|
||||||
partsResult := gjson.Get(clientContentJSON, "parts")
|
partsResult := gjson.GetBytes(clientContentJSON, "parts")
|
||||||
if partsResult.IsArray() {
|
if partsResult.IsArray() {
|
||||||
parts := partsResult.Array()
|
parts := partsResult.Array()
|
||||||
var thinkingParts []gjson.Result
|
var thinkingParts []gjson.Result
|
||||||
@@ -354,7 +354,7 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
for _, p := range otherParts {
|
for _, p := range otherParts {
|
||||||
newParts = append(newParts, p.Value())
|
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:
|
// Skip messages with empty parts array to avoid Gemini API error:
|
||||||
// "required oneof field 'data' must have one initialized field"
|
// "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 {
|
if !partsCheck.IsArray() || len(partsCheck.Array()) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
contentsJSON, _ = sjson.SetRaw(contentsJSON, "-1", clientContentJSON)
|
contentsJSON, _ = sjson.SetRawBytes(contentsJSON, "-1", clientContentJSON)
|
||||||
hasContents = true
|
hasContents = true
|
||||||
} else if contentsResult.Type == gjson.String {
|
} else if contentsResult.Type == gjson.String {
|
||||||
prompt := contentsResult.String()
|
prompt := contentsResult.String()
|
||||||
partJSON := `{}`
|
partJSON := []byte(`{}`)
|
||||||
if prompt != "" {
|
if prompt != "" {
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", prompt)
|
partJSON, _ = sjson.SetBytes(partJSON, "text", prompt)
|
||||||
}
|
}
|
||||||
clientContentJSON, _ = sjson.SetRaw(clientContentJSON, "parts.-1", partJSON)
|
clientContentJSON, _ = sjson.SetRawBytes(clientContentJSON, "parts.-1", partJSON)
|
||||||
contentsJSON, _ = sjson.SetRaw(contentsJSON, "-1", clientContentJSON)
|
contentsJSON, _ = sjson.SetRawBytes(contentsJSON, "-1", clientContentJSON)
|
||||||
hasContents = true
|
hasContents = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tools
|
// tools
|
||||||
toolsJSON := ""
|
var toolsJSON []byte
|
||||||
toolDeclCount := 0
|
toolDeclCount := 0
|
||||||
allowedToolKeys := []string{"name", "description", "behavior", "parameters", "parametersJsonSchema", "response", "responseJsonSchema"}
|
allowedToolKeys := []string{"name", "description", "behavior", "parameters", "parametersJsonSchema", "response", "responseJsonSchema"}
|
||||||
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
toolsResult := gjson.GetBytes(rawJSON, "tools")
|
||||||
if toolsResult.IsArray() {
|
if toolsResult.IsArray() {
|
||||||
toolsJSON = `[{"functionDeclarations":[]}]`
|
toolsJSON = []byte(`[{"functionDeclarations":[]}]`)
|
||||||
toolsResults := toolsResult.Array()
|
toolsResults := toolsResult.Array()
|
||||||
for i := 0; i < len(toolsResults); i++ {
|
for i := 0; i < len(toolsResults); i++ {
|
||||||
toolResult := toolsResults[i]
|
toolResult := toolsResults[i]
|
||||||
@@ -396,23 +396,23 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
// Sanitize the input schema for Antigravity API compatibility
|
// Sanitize the input schema for Antigravity API compatibility
|
||||||
inputSchema := util.CleanJSONSchemaForAntigravity(inputSchemaResult.Raw)
|
inputSchema := util.CleanJSONSchemaForAntigravity(inputSchemaResult.Raw)
|
||||||
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
tool, _ := sjson.DeleteBytes([]byte(toolResult.Raw), "input_schema")
|
||||||
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
|
tool, _ = sjson.SetRawBytes(tool, "parametersJsonSchema", []byte(inputSchema))
|
||||||
for toolKey := range gjson.Parse(tool).Map() {
|
for toolKey := range gjson.ParseBytes(tool).Map() {
|
||||||
if util.InArray(allowedToolKeys, toolKey) {
|
if util.InArray(allowedToolKeys, toolKey) {
|
||||||
continue
|
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++
|
toolDeclCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
// Build output Gemini CLI request JSON
|
||||||
out := `{"model":"","request":{"contents":[]}}`
|
out := []byte(`{"model":"","request":{"contents":[]}}`)
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// Inject interleaved thinking hint when both tools and thinking are active
|
// Inject interleaved thinking hint when both tools and thinking are active
|
||||||
hasTools := toolDeclCount > 0
|
hasTools := toolDeclCount > 0
|
||||||
@@ -426,27 +426,27 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
if hasSystemInstruction {
|
if hasSystemInstruction {
|
||||||
// Append hint as a new part to existing system instruction
|
// Append hint as a new part to existing system instruction
|
||||||
hintPart := `{"text":""}`
|
hintPart := []byte(`{"text":""}`)
|
||||||
hintPart, _ = sjson.Set(hintPart, "text", interleavedHint)
|
hintPart, _ = sjson.SetBytes(hintPart, "text", interleavedHint)
|
||||||
systemInstructionJSON, _ = sjson.SetRaw(systemInstructionJSON, "parts.-1", hintPart)
|
systemInstructionJSON, _ = sjson.SetRawBytes(systemInstructionJSON, "parts.-1", hintPart)
|
||||||
} else {
|
} else {
|
||||||
// Create new system instruction with hint
|
// Create new system instruction with hint
|
||||||
systemInstructionJSON = `{"role":"user","parts":[]}`
|
systemInstructionJSON = []byte(`{"role":"user","parts":[]}`)
|
||||||
hintPart := `{"text":""}`
|
hintPart := []byte(`{"text":""}`)
|
||||||
hintPart, _ = sjson.Set(hintPart, "text", interleavedHint)
|
hintPart, _ = sjson.SetBytes(hintPart, "text", interleavedHint)
|
||||||
systemInstructionJSON, _ = sjson.SetRaw(systemInstructionJSON, "parts.-1", hintPart)
|
systemInstructionJSON, _ = sjson.SetRawBytes(systemInstructionJSON, "parts.-1", hintPart)
|
||||||
hasSystemInstruction = true
|
hasSystemInstruction = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasSystemInstruction {
|
if hasSystemInstruction {
|
||||||
out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstructionJSON)
|
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", systemInstructionJSON)
|
||||||
}
|
}
|
||||||
if hasContents {
|
if hasContents {
|
||||||
out, _ = sjson.SetRaw(out, "request.contents", contentsJSON)
|
out, _ = sjson.SetRawBytes(out, "request.contents", contentsJSON)
|
||||||
}
|
}
|
||||||
if toolDeclCount > 0 {
|
if toolDeclCount > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "request.tools", toolsJSON)
|
out, _ = sjson.SetRawBytes(out, "request.tools", toolsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// tool_choice
|
// tool_choice
|
||||||
@@ -463,15 +463,15 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
switch toolChoiceType {
|
switch toolChoiceType {
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "AUTO")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "AUTO")
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "NONE")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "NONE")
|
||||||
case "any":
|
case "any":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
case "tool":
|
case "tool":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
if toolChoiceName != "" {
|
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":
|
case "enabled":
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
budget := int(b.Int())
|
budget := int(b.Int())
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// For adaptive thinking:
|
// For adaptive thinking:
|
||||||
@@ -495,28 +495,27 @@ func ConvertClaudeRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort)
|
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort)
|
||||||
} else {
|
} 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 {
|
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 {
|
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 {
|
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 {
|
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)
|
out = common.AttachDefaultSafetySettings(out, "request.safetySettings")
|
||||||
outBytes = common.AttachDefaultSafetySettings(outBytes, "request.safetySettings")
|
|
||||||
|
|
||||||
return outBytes
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/cache"
|
"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"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
@@ -63,8 +64,8 @@ var toolUseIDCounter uint64
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Claude Code-compatible JSON response
|
// - [][]byte: A slice of bytes, each containing a Claude Code-compatible SSE payload.
|
||||||
func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &Params{
|
*param = &Params{
|
||||||
HasFirstResponse: false,
|
HasFirstResponse: false,
|
||||||
@@ -77,44 +78,44 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
params := (*param).(*Params)
|
params := (*param).(*Params)
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
output := ""
|
output := make([]byte, 0, 256)
|
||||||
// Only send final events if we have actually output content
|
// Only send final events if we have actually output content
|
||||||
if params.HasContent {
|
if params.HasContent {
|
||||||
appendFinalEvents(params, &output, true)
|
appendFinalEvents(params, &output, true)
|
||||||
return []string{
|
output = translatorcommon.AppendSSEEventString(output, "message_stop", `{"type":"message_stop"}`, 3)
|
||||||
output + "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
|
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
|
// Initialize the streaming session with a message_start event
|
||||||
// This is only sent for the very first response chunk to establish the streaming session
|
// This is only sent for the very first response chunk to establish the streaming session
|
||||||
if !params.HasFirstResponse {
|
if !params.HasFirstResponse {
|
||||||
output = "event: message_start\n"
|
|
||||||
|
|
||||||
// Create the initial message structure with default values according to Claude Code API specification
|
// 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
|
// 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.
|
// Use cpaUsageMetadata within the message_start event for Claude.
|
||||||
if promptTokenCount := gjson.GetBytes(rawJSON, "response.cpaUsageMetadata.promptTokenCount"); promptTokenCount.Exists() {
|
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() {
|
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
|
// Override default values with actual response metadata if available from the Gemini CLI response
|
||||||
if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() {
|
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() {
|
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
|
params.HasFirstResponse = true
|
||||||
}
|
}
|
||||||
@@ -144,15 +145,13 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
params.CurrentThinkingText.Reset()
|
params.CurrentThinkingText.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
output = output + "event: content_block_delta\n"
|
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()))
|
||||||
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()))
|
appendEvent("content_block_delta", string(data))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
} else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state
|
} else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state
|
||||||
params.CurrentThinkingText.WriteString(partTextResult.String())
|
params.CurrentThinkingText.WriteString(partTextResult.String())
|
||||||
output = output + "event: content_block_delta\n"
|
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partTextResult.String())
|
||||||
data, _ := sjson.Set(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))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
} else {
|
} else {
|
||||||
// Transition from another state to thinking
|
// 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 + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, params.ResponseIndex)
|
||||||
// output = output + "\n\n\n"
|
// output = output + "\n\n\n"
|
||||||
}
|
}
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
params.ResponseIndex++
|
params.ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new thinking content block
|
// Start a new thinking content block
|
||||||
output = output + "event: content_block_start\n"
|
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"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())
|
||||||
output = output + "\n\n\n"
|
appendEvent("content_block_delta", string(data))
|
||||||
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)
|
|
||||||
params.ResponseType = 2 // Set state to thinking
|
params.ResponseType = 2 // Set state to thinking
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
// Start accumulating thinking text for signature caching
|
// Start accumulating thinking text for signature caching
|
||||||
@@ -188,9 +182,8 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
// Process regular text content (user-visible output)
|
// Process regular text content (user-visible output)
|
||||||
// Continue existing text block if already in content state
|
// Continue existing text block if already in content state
|
||||||
if params.ResponseType == 1 {
|
if params.ResponseType == 1 {
|
||||||
output = output + "event: content_block_delta\n"
|
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, params.ResponseIndex)), "delta.text", partTextResult.String())
|
||||||
data, _ := sjson.Set(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))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
} else {
|
} else {
|
||||||
// Transition from another state to text content
|
// 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 + fmt.Sprintf(`data: {"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":null}}`, params.ResponseIndex)
|
||||||
// output = output + "\n\n\n"
|
// output = output + "\n\n\n"
|
||||||
}
|
}
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
params.ResponseIndex++
|
params.ResponseIndex++
|
||||||
}
|
}
|
||||||
if partTextResult.String() != "" {
|
if partTextResult.String() != "" {
|
||||||
// Start a new text content block
|
// Start a new text content block
|
||||||
output = output + "event: content_block_start\n"
|
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, params.ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"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())
|
||||||
output = output + "\n\n\n"
|
appendEvent("content_block_delta", string(data))
|
||||||
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)
|
|
||||||
params.ResponseType = 1 // Set state to content
|
params.ResponseType = 1 // Set state to content
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
}
|
}
|
||||||
@@ -229,9 +217,7 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
// Handle state transitions when switching to function calls
|
// Handle state transitions when switching to function calls
|
||||||
// Close any existing function call block first
|
// Close any existing function call block first
|
||||||
if params.ResponseType == 3 {
|
if params.ResponseType == 3 {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
params.ResponseIndex++
|
params.ResponseIndex++
|
||||||
params.ResponseType = 0
|
params.ResponseType = 0
|
||||||
}
|
}
|
||||||
@@ -245,26 +231,21 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
|
|
||||||
// Close any other existing content block
|
// Close any other existing content block
|
||||||
if params.ResponseType != 0 {
|
if params.ResponseType != 0 {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
params.ResponseIndex++
|
params.ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new tool use content block
|
// Start a new tool use content block
|
||||||
// This creates the structure for a function call in Claude Code format
|
// 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
|
// 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 := []byte(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.SetBytes(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)
|
data, _ = sjson.SetBytes(data, "content_block.name", fcName)
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
appendEvent("content_block_start", string(data))
|
||||||
|
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
output = output + "event: content_block_delta\n"
|
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)
|
||||||
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)
|
appendEvent("content_block_delta", string(data))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
}
|
}
|
||||||
params.ResponseType = 3
|
params.ResponseType = 3
|
||||||
params.HasContent = true
|
params.HasContent = true
|
||||||
@@ -296,10 +277,10 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
|
|||||||
appendFinalEvents(params, &output, false)
|
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 {
|
if params.HasSentFinalEvents {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -314,9 +295,7 @@ func appendFinalEvents(params *Params, output *string, force bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if params.ResponseType != 0 {
|
if params.ResponseType != 0 {
|
||||||
*output = *output + "event: content_block_stop\n"
|
*output = translatorcommon.AppendSSEEventString(*output, "content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex), 3)
|
||||||
*output = *output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, params.ResponseIndex)
|
|
||||||
*output = *output + "\n\n\n"
|
|
||||||
params.ResponseType = 0
|
params.ResponseType = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,18 +308,16 @@ func appendFinalEvents(params *Params, output *string, force bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
*output = *output + "event: message_delta\n"
|
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))
|
||||||
*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)
|
|
||||||
// Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working)
|
// Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working)
|
||||||
if params.CachedTokenCount > 0 {
|
if params.CachedTokenCount > 0 {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Warnf("antigravity claude response: failed to set cache_read_input_tokens: %v", err)
|
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
|
params.HasSentFinalEvents = true
|
||||||
}
|
}
|
||||||
@@ -369,8 +346,8 @@ func resolveStopReason(params *Params) string {
|
|||||||
// - param: A pointer to a parameter object for the conversion.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Claude-compatible JSON response.
|
// - []byte: A Claude-compatible JSON response.
|
||||||
func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
_ = originalRequestRawJSON
|
_ = originalRequestRawJSON
|
||||||
modelName := gjson.GetBytes(requestRawJSON, "model").String()
|
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 := []byte(`{"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.SetBytes(responseJSON, "id", root.Get("response.responseId").String())
|
||||||
responseJSON, _ = sjson.Set(responseJSON, "model", root.Get("response.modelVersion").String())
|
responseJSON, _ = sjson.SetBytes(responseJSON, "model", root.Get("response.modelVersion").String())
|
||||||
responseJSON, _ = sjson.Set(responseJSON, "usage.input_tokens", promptTokens)
|
responseJSON, _ = sjson.SetBytes(responseJSON, "usage.input_tokens", promptTokens)
|
||||||
responseJSON, _ = sjson.Set(responseJSON, "usage.output_tokens", outputTokens)
|
responseJSON, _ = sjson.SetBytes(responseJSON, "usage.output_tokens", outputTokens)
|
||||||
// Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working)
|
// Add cache_read_input_tokens if cached tokens are present (indicates prompt caching is working)
|
||||||
if cachedTokens > 0 {
|
if cachedTokens > 0 {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Warnf("antigravity claude response: failed to set cache_read_input_tokens: %v", err)
|
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 {
|
if contentArrayInitialized {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
responseJSON, _ = sjson.SetRaw(responseJSON, "content", "[]")
|
responseJSON, _ = sjson.SetRawBytes(responseJSON, "content", []byte("[]"))
|
||||||
contentArrayInitialized = true
|
contentArrayInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -423,9 +400,9 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ensureContentArray()
|
ensureContentArray()
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", textBuilder.String())
|
block, _ = sjson.SetBytes(block, "text", textBuilder.String())
|
||||||
responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", block)
|
responseJSON, _ = sjson.SetRawBytes(responseJSON, "content.-1", block)
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,12 +411,12 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ensureContentArray()
|
ensureContentArray()
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
||||||
if thinkingSignature != "" {
|
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()
|
thinkingBuilder.Reset()
|
||||||
thinkingSignature = ""
|
thinkingSignature = ""
|
||||||
}
|
}
|
||||||
@@ -475,16 +452,16 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
|
|||||||
|
|
||||||
name := functionCall.Get("name").String()
|
name := functionCall.Get("name").String()
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
toolBlock, _ = sjson.SetBytes(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
toolBlock, _ = sjson.SetBytes(toolBlock, "name", name)
|
||||||
|
|
||||||
if args := functionCall.Get("args"); args.Exists() && args.Raw != "" && gjson.Valid(args.Raw) && args.IsObject() {
|
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()
|
ensureContentArray()
|
||||||
responseJSON, _ = sjson.SetRaw(responseJSON, "content.-1", toolBlock)
|
responseJSON, _ = sjson.SetRawBytes(responseJSON, "content.-1", toolBlock)
|
||||||
continue
|
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 promptTokens == 0 && outputTokens == 0 {
|
||||||
if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() {
|
if usageMeta := root.Get("response.usageMetadata"); !usageMeta.Exists() {
|
||||||
responseJSON, _ = sjson.Delete(responseJSON, "usage")
|
responseJSON, _ = sjson.DeleteBytes(responseJSON, "usage")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return responseJSON
|
return responseJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ import (
|
|||||||
// - []byte: The transformed request data in Gemini API format
|
// - []byte: The transformed request data in Gemini API format
|
||||||
func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
template := ""
|
template := `{"project":"","request":{},"model":""}`
|
||||||
template = `{"project":"","request":{},"model":""}`
|
templateBytes, _ := sjson.SetRawBytes([]byte(template), "request", rawJSON)
|
||||||
template, _ = sjson.SetRaw(template, "request", string(rawJSON))
|
templateBytes, _ = sjson.SetBytes(templateBytes, "model", modelName)
|
||||||
template, _ = sjson.Set(template, "model", modelName)
|
template = string(templateBytes)
|
||||||
template, _ = sjson.Delete(template, "request.model")
|
template, _ = sjson.Delete(template, "request.model")
|
||||||
|
|
||||||
template, errFixCLIToolResponse := fixCLIToolResponse(template)
|
template, errFixCLIToolResponse := fixCLIToolResponse(template)
|
||||||
@@ -47,7 +47,8 @@ func ConvertGeminiRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
|
|
||||||
systemInstructionResult := gjson.Get(template, "request.system_instruction")
|
systemInstructionResult := gjson.Get(template, "request.system_instruction")
|
||||||
if systemInstructionResult.Exists() {
|
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")
|
template, _ = sjson.Delete(template, "request.system_instruction")
|
||||||
}
|
}
|
||||||
rawJSON = []byte(template)
|
rawJSON = []byte(template)
|
||||||
@@ -149,7 +150,8 @@ func parseFunctionResponseRaw(response gjson.Result, fallbackName string) string
|
|||||||
raw := response.Raw
|
raw := response.Raw
|
||||||
name := response.Get("functionResponse.name").String()
|
name := response.Get("functionResponse.name").String()
|
||||||
if strings.TrimSpace(name) == "" && fallbackName != "" {
|
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
|
return raw
|
||||||
}
|
}
|
||||||
@@ -157,27 +159,27 @@ func parseFunctionResponseRaw(response gjson.Result, fallbackName string) string
|
|||||||
log.Debugf("parse function response failed, using fallback")
|
log.Debugf("parse function response failed, using fallback")
|
||||||
funcResp := response.Get("functionResponse")
|
funcResp := response.Get("functionResponse")
|
||||||
if funcResp.Exists() {
|
if funcResp.Exists() {
|
||||||
fr := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
fr := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
|
||||||
name := funcResp.Get("name").String()
|
name := funcResp.Get("name").String()
|
||||||
if strings.TrimSpace(name) == "" {
|
if strings.TrimSpace(name) == "" {
|
||||||
name = fallbackName
|
name = fallbackName
|
||||||
}
|
}
|
||||||
fr, _ = sjson.Set(fr, "functionResponse.name", name)
|
fr, _ = sjson.SetBytes(fr, "functionResponse.name", name)
|
||||||
fr, _ = sjson.Set(fr, "functionResponse.response.result", funcResp.Get("response").String())
|
fr, _ = sjson.SetBytes(fr, "functionResponse.response.result", funcResp.Get("response").String())
|
||||||
if id := funcResp.Get("id").String(); id != "" {
|
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
|
useName := fallbackName
|
||||||
if useName == "" {
|
if useName == "" {
|
||||||
useName = "unknown"
|
useName = "unknown"
|
||||||
}
|
}
|
||||||
fr := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
fr := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
|
||||||
fr, _ = sjson.Set(fr, "functionResponse.name", useName)
|
fr, _ = sjson.SetBytes(fr, "functionResponse.name", useName)
|
||||||
fr, _ = sjson.Set(fr, "functionResponse.response.result", response.String())
|
fr, _ = sjson.SetBytes(fr, "functionResponse.response.result", response.String())
|
||||||
return fr
|
return string(fr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixCLIToolResponse performs sophisticated tool response format conversion and grouping.
|
// 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
|
// Initialize data structures for processing and grouping
|
||||||
contentsWrapper := `{"contents":[]}`
|
contentsWrapper := []byte(`{"contents":[]}`)
|
||||||
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
||||||
var collectedResponses []gjson.Result // Standalone responses to be matched
|
var collectedResponses []gjson.Result // Standalone responses to be matched
|
||||||
|
|
||||||
@@ -237,16 +239,16 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
// Create merged function response content
|
// Create merged function response content
|
||||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
functionResponseContent := []byte(`{"parts":[],"role":"function"}`)
|
||||||
for ri, response := range groupResponses {
|
for ri, response := range groupResponses {
|
||||||
partRaw := parseFunctionResponseRaw(response, group.CallNames[ri])
|
partRaw := parseFunctionResponseRaw(response, group.CallNames[ri])
|
||||||
if partRaw != "" {
|
if partRaw != "" {
|
||||||
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
|
functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts.-1", []byte(partRaw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +271,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
log.Warnf("failed to parse model content")
|
log.Warnf("failed to parse model content")
|
||||||
return true
|
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
|
// Create a new group for tracking responses
|
||||||
group := &FunctionCallGroup{
|
group := &FunctionCallGroup{
|
||||||
@@ -283,7 +285,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
log.Warnf("failed to parse content")
|
log.Warnf("failed to parse content")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-model content (user, etc.)
|
// Non-model content (user, etc.)
|
||||||
@@ -291,7 +293,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
log.Warnf("failed to parse content")
|
log.Warnf("failed to parse content")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -303,23 +305,22 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
functionResponseContent := []byte(`{"parts":[],"role":"function"}`)
|
||||||
for ri, response := range groupResponses {
|
for ri, response := range groupResponses {
|
||||||
partRaw := parseFunctionResponseRaw(response, group.CallNames[ri])
|
partRaw := parseFunctionResponseRaw(response, group.CallNames[ri])
|
||||||
if partRaw != "" {
|
if partRaw != "" {
|
||||||
functionResponseContent, _ = sjson.SetRaw(functionResponseContent, "parts.-1", partRaw)
|
functionResponseContent, _ = sjson.SetRawBytes(functionResponseContent, "parts.-1", []byte(partRaw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if gjson.Get(functionResponseContent, "parts.#").Int() > 0 {
|
if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the original JSON with the new contents
|
// Update the original JSON with the new contents
|
||||||
result := input
|
result, _ := sjson.SetRawBytes([]byte(input), "request.contents", []byte(gjson.GetBytes(contentsWrapper, "contents").Raw))
|
||||||
result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
|
|
||||||
|
|
||||||
return result, nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -29,8 +29,8 @@ import (
|
|||||||
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: The transformed request data in Gemini API format
|
// - [][]byte: The transformed response data in Gemini API format.
|
||||||
func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string {
|
func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte {
|
||||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
}
|
}
|
||||||
@@ -44,22 +44,22 @@ func ConvertAntigravityResponseToGemini(ctx context.Context, _ string, originalR
|
|||||||
chunk = restoreUsageMetadata(chunk)
|
chunk = restoreUsageMetadata(chunk)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chunkTemplate := "[]"
|
chunkTemplate := []byte("[]")
|
||||||
responseResult := gjson.ParseBytes(chunk)
|
responseResult := gjson.ParseBytes(chunk)
|
||||||
if responseResult.IsArray() {
|
if responseResult.IsArray() {
|
||||||
responseResultItems := responseResult.Array()
|
responseResultItems := responseResult.Array()
|
||||||
for i := 0; i < len(responseResultItems); i++ {
|
for i := 0; i < len(responseResultItems); i++ {
|
||||||
responseResultItem := responseResultItems[i]
|
responseResultItem := responseResultItems[i]
|
||||||
if responseResultItem.Get("response").Exists() {
|
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.
|
// 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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response containing the response data
|
// - []byte: A Gemini-compatible JSON response containing the response data.
|
||||||
func ConvertAntigravityResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertAntigravityResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
responseResult := gjson.GetBytes(rawJSON, "response")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
chunk := restoreUsageMetadata([]byte(responseResult.Raw))
|
chunk := restoreUsageMetadata([]byte(responseResult.Raw))
|
||||||
return string(chunk)
|
return chunk
|
||||||
}
|
}
|
||||||
return string(rawJSON)
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreUsageMetadata renames cpaUsageMetadata back to usageMetadata.
|
// restoreUsageMetadata renames cpaUsageMetadata back to usageMetadata.
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ func TestConvertAntigravityResponseToGeminiNonStream(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result := ConvertAntigravityResponseToGeminiNonStream(context.Background(), "", nil, nil, tt.input, nil)
|
result := ConvertAntigravityResponseToGeminiNonStream(context.Background(), "", nil, nil, tt.input, nil)
|
||||||
if result != tt.expected {
|
if string(result) != tt.expected {
|
||||||
t.Errorf("ConvertAntigravityResponseToGeminiNonStream() = %s, want %s", 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 {
|
if len(results) != 1 {
|
||||||
t.Fatalf("expected 1 result, got %d", len(results))
|
t.Fatalf("expected 1 result, got %d", len(results))
|
||||||
}
|
}
|
||||||
if results[0] != tt.expected {
|
if string(results[0]) != tt.expected {
|
||||||
t.Errorf("ConvertAntigravityResponseToGemini() = %s, want %s", results[0], tt.expected)
|
t.Errorf("ConvertAntigravityResponseToGemini() = %s, want %s", string(results[0]), tt.expected)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,31 +354,35 @@ func ConvertOpenAIRequestToAntigravity(modelName string, inputRawJSON []byte, _
|
|||||||
if errRename != nil {
|
if errRename != nil {
|
||||||
log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename)
|
log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename)
|
||||||
var errSet error
|
var errSet error
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object")
|
fnRawBytes, errSet := sjson.SetBytes([]byte(fnRaw), "parametersJsonSchema.type", "object")
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
fnRaw = string(fnRawBytes)
|
||||||
|
fnRawBytes, errSet = sjson.SetRawBytes([]byte(fnRaw), "parametersJsonSchema.properties", []byte(`{}`))
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fnRaw = string(fnRawBytes)
|
||||||
} else {
|
} else {
|
||||||
fnRaw = renamed
|
fnRaw = renamed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var errSet error
|
var errSet error
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object")
|
fnRawBytes, errSet := sjson.SetBytes([]byte(fnRaw), "parametersJsonSchema.type", "object")
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
fnRaw = string(fnRawBytes)
|
||||||
|
fnRawBytes, errSet = sjson.SetRawBytes([]byte(fnRaw), "parametersJsonSchema.properties", []byte(`{}`))
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fnRaw = string(fnRawBytes)
|
||||||
}
|
}
|
||||||
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
||||||
if !hasFunction {
|
if !hasFunction {
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ var functionCallIDCounter uint64
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
// - [][]byte: A slice of OpenAI-compatible JSON responses
|
||||||
func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &convertCliResponseToOpenAIChatParams{
|
*param = &convertCliResponseToOpenAIChatParams{
|
||||||
UnixTimestamp: 0,
|
UnixTimestamp: 0,
|
||||||
@@ -54,15 +54,15 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the OpenAI SSE template.
|
// 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.
|
// Extract and set the model version.
|
||||||
if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() {
|
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.
|
// Extract and set the creation timestamp.
|
||||||
@@ -71,14 +71,14 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp = t.Unix()
|
(*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp = t.Unix()
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set the response ID.
|
// Extract and set the response ID.
|
||||||
if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() {
|
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)
|
// 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() {
|
if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() {
|
||||||
cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
|
cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
|
||||||
if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.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() {
|
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()
|
promptTokenCount := usageResult.Get("promptTokenCount").Int()
|
||||||
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").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 {
|
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)
|
// Include cached token count if present (indicates prompt caching is working)
|
||||||
if cachedTokenCount > 0 {
|
if cachedTokenCount > 0 {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Warnf("antigravity openai response: failed to set cached_tokens: %v", err)
|
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.
|
// Handle text content, distinguishing between regular content and reasoning/thoughts.
|
||||||
if partResult.Get("thought").Bool() {
|
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 {
|
} 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() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function call content.
|
// Handle function call content.
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).SawToolCall = true // Persist across chunks
|
(*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
|
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
||||||
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
||||||
functionCallIndex = len(toolCallsResult.Array())
|
functionCallIndex = len(toolCallsResult.Array())
|
||||||
} else {
|
} 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()
|
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.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
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.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
||||||
} else if inlineDataResult.Exists() {
|
} else if inlineDataResult.Exists() {
|
||||||
data := inlineDataResult.Get("data").String()
|
data := inlineDataResult.Get("data").String()
|
||||||
if data == "" {
|
if data == "" {
|
||||||
@@ -181,16 +181,16 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
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() {
|
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())
|
imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array())
|
||||||
imagePayload := `{"type":"image_url","image_url":{"url":""}}`
|
imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL)
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,11 +212,11 @@ func ConvertAntigravityResponseToOpenAI(_ context.Context, _ string, originalReq
|
|||||||
} else {
|
} else {
|
||||||
finishReason = "stop"
|
finishReason = "stop"
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason)
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason)
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", strings.ToLower(upstreamFinishReason))
|
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.
|
// 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
|
// - param: A pointer to a parameter object for the conversion
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []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) string {
|
func ConvertAntigravityResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||||
responseResult := gjson.GetBytes(rawJSON, "response")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
return ConvertGeminiResponseToOpenAINonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, []byte(responseResult.Raw), param)
|
return ConvertGeminiResponseToOpenAINonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, []byte(responseResult.Raw), param)
|
||||||
}
|
}
|
||||||
return ""
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestFinishReasonToolCallsNotOverwritten(t *testing.T) {
|
|||||||
if len(result1) != 1 {
|
if len(result1) != 1 {
|
||||||
t.Fatalf("Expected 1 result from chunk1, got %d", len(result1))
|
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" {
|
if fr1.Exists() && fr1.String() != "" && fr1.Type.String() != "Null" {
|
||||||
t.Errorf("Expected finish_reason to be null in chunk1, got: %v", fr1.String())
|
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 {
|
if len(result2) != 1 {
|
||||||
t.Fatalf("Expected 1 result from chunk2, got %d", len(result2))
|
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" {
|
if fr2 != "tool_calls" {
|
||||||
t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr2)
|
t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify native_finish_reason is lowercase upstream value
|
// 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" {
|
if nfr2 != "stop" {
|
||||||
t.Errorf("Expected native_finish_reason 'stop', got: %s", nfr2)
|
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)
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
// Verify finish_reason is "stop" (no tool calls were made)
|
// 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" {
|
if fr != "stop" {
|
||||||
t.Errorf("Expected finish_reason 'stop', got: %s", fr)
|
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)
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
// Verify finish_reason is "max_tokens"
|
// 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" {
|
if fr != "max_tokens" {
|
||||||
t.Errorf("Expected finish_reason 'max_tokens', got: %s", fr)
|
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)
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
// Verify finish_reason is "tool_calls" (takes priority over max_tokens)
|
// 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" {
|
if fr != "tool_calls" {
|
||||||
t.Errorf("Expected finish_reason 'tool_calls', got: %s", fr)
|
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)
|
result1 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk1, ¶m)
|
||||||
|
|
||||||
// Verify no finish_reason on intermediate chunk
|
// 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" {
|
if fr1.Exists() && fr1.String() != "" && fr1.Type.String() != "Null" {
|
||||||
t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr1)
|
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)
|
result2 := ConvertAntigravityResponseToOpenAI(ctx, "model", nil, nil, chunk2, ¶m)
|
||||||
|
|
||||||
// Verify no finish_reason on intermediate chunk
|
// 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" {
|
if fr2.Exists() && fr2.String() != "" && fr2.Type.String() != "Null" {
|
||||||
t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr2)
|
t.Errorf("Expected no finish_reason on intermediate chunk, got: %v", fr2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"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")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
rawJSON = []byte(responseResult.Raw)
|
rawJSON = []byte(responseResult.Raw)
|
||||||
@@ -15,7 +15,7 @@ func ConvertAntigravityResponseToOpenAIResponses(ctx context.Context, modelName
|
|||||||
return ConvertGeminiResponseToOpenAIResponses(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
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")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
rawJSON = []byte(responseResult.Raw)
|
rawJSON = []byte(responseResult.Raw)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/claude/gemini"
|
. "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.
|
// 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
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object
|
// - [][]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) []string {
|
func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
outputs := ConvertClaudeResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
outputs := ConvertClaudeResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
// Wrap each converted response in a "response" object to match Gemini CLI API structure
|
// 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++ {
|
for i := 0; i < len(outputs); i++ {
|
||||||
json := `{"response": {}}`
|
newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i]))
|
||||||
output, _ := sjson.SetRaw(json, "response", outputs[i])
|
|
||||||
newOutputs = append(newOutputs, output)
|
|
||||||
}
|
}
|
||||||
return newOutputs
|
return newOutputs
|
||||||
}
|
}
|
||||||
@@ -47,15 +45,13 @@ func ConvertClaudeResponseToGeminiCLI(ctx context.Context, modelName string, ori
|
|||||||
// - param: A pointer to a parameter object for the conversion
|
// - param: A pointer to a parameter object for the conversion
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response wrapped in a response object
|
// - []byte: A Gemini-compatible JSON response wrapped in a response object
|
||||||
func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
func ConvertClaudeResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||||
strJSON := ConvertClaudeResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
out := ConvertClaudeResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
// Wrap the converted response in a "response" object to match Gemini CLI API structure
|
// Wrap the converted response in a "response" object to match Gemini CLI API structure
|
||||||
json := `{"response": {}}`
|
return translatorcommon.WrapGeminiCLIResponse(out)
|
||||||
strJSON, _ = sjson.SetRaw(json, "response", strJSON)
|
|
||||||
return strJSON
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiCLITokenCount(ctx context.Context, count int64) string {
|
func GeminiCLITokenCount(ctx context.Context, count int64) []byte {
|
||||||
return GeminiTokenCount(ctx, count)
|
return GeminiTokenCount(ctx, count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
||||||
|
|
||||||
// Base Claude message payload
|
// 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)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
@@ -87,20 +87,20 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
var pendingToolIDs []string
|
var pendingToolIDs []string
|
||||||
|
|
||||||
// Model mapping to specify which Claude Code model to use
|
// 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
|
// Generation config extraction from Gemini format
|
||||||
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
||||||
// Max output tokens configuration
|
// Max output tokens configuration
|
||||||
if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
|
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
|
// Temperature setting for controlling response randomness
|
||||||
if temp := genConfig.Get("temperature"); temp.Exists() {
|
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() {
|
} else if topP := genConfig.Get("topP"); topP.Exists() {
|
||||||
// Top P setting for nucleus sampling (filtered out if temperature is set)
|
// 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
|
// Stop sequences configuration for custom termination conditions
|
||||||
if stopSeqs := genConfig.Get("stopSequences"); stopSeqs.Exists() && stopSeqs.IsArray() {
|
if stopSeqs := genConfig.Get("stopSequences"); stopSeqs.Exists() && stopSeqs.IsArray() {
|
||||||
@@ -110,7 +110,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if len(stopSequences) > 0 {
|
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
|
// Include thoughts configuration for reasoning process visibility
|
||||||
@@ -132,30 +132,30 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
switch level {
|
switch level {
|
||||||
case "":
|
case "":
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.DeleteBytes(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok {
|
if mapped, ok := thinking.MapToClaudeEffort(level, supportsMax); ok {
|
||||||
level = mapped
|
level = mapped
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Set(out, "output_config.effort", level)
|
out, _ = sjson.SetBytes(out, "output_config.effort", level)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch level {
|
switch level {
|
||||||
case "":
|
case "":
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
default:
|
default:
|
||||||
if budget, ok := thinking.ConvertLevelToBudget(level); ok {
|
if budget, ok := thinking.ConvertLevelToBudget(level); ok {
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,37 +169,37 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
if supportsAdaptive {
|
if supportsAdaptive {
|
||||||
switch budget {
|
switch budget {
|
||||||
case 0:
|
case 0:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.DeleteBytes(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
level, ok := thinking.ConvertBudgetToLevel(budget)
|
level, ok := thinking.ConvertBudgetToLevel(budget)
|
||||||
if ok {
|
if ok {
|
||||||
if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM {
|
if mapped, okM := thinking.MapToClaudeEffort(level, supportsMax); okM {
|
||||||
level = mapped
|
level = mapped
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Set(out, "output_config.effort", level)
|
out, _ = sjson.SetBytes(out, "output_config.effort", level)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch budget {
|
switch budget {
|
||||||
case 0:
|
case 0:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
case -1:
|
case -1:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
default:
|
default:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if includeThoughts := thinkingConfig.Get("includeThoughts"); includeThoughts.Exists() && includeThoughts.Type == gjson.True {
|
} 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 {
|
} 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 {
|
if systemText.Len() > 0 {
|
||||||
// Create system message in Claude Code format
|
// Create system message in Claude Code format
|
||||||
systemMessage := `{"role":"user","content":[{"type":"text","text":""}]}`
|
systemMessage := []byte(`{"role":"user","content":[{"type":"text","text":""}]}`)
|
||||||
systemMessage, _ = sjson.Set(systemMessage, "content.0.text", systemText.String())
|
systemMessage, _ = sjson.SetBytes(systemMessage, "content.0.text", systemText.String())
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", systemMessage)
|
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
|
// Create message structure in Claude Code format
|
||||||
msg := `{"role":"","content":[]}`
|
msg := []byte(`{"role":"","content":[]}`)
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||||
|
|
||||||
if parts := content.Get("parts"); parts.Exists() && parts.IsArray() {
|
if parts := content.Get("parts"); parts.Exists() && parts.IsArray() {
|
||||||
parts.ForEach(func(_, part gjson.Result) bool {
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
// Text content conversion
|
// Text content conversion
|
||||||
if text := part.Get("text"); text.Exists() {
|
if text := part.Get("text"); text.Exists() {
|
||||||
textContent := `{"type":"text","text":""}`
|
textContent := []byte(`{"type":"text","text":""}`)
|
||||||
textContent, _ = sjson.Set(textContent, "text", text.String())
|
textContent, _ = sjson.SetBytes(textContent, "text", text.String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", textContent)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", textContent)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function call (from model/assistant) conversion to tool use
|
// Function call (from model/assistant) conversion to tool use
|
||||||
if fc := part.Get("functionCall"); fc.Exists() && role == "assistant" {
|
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
|
// Generate a unique tool ID and enqueue it for later matching
|
||||||
// with the corresponding functionResponse
|
// with the corresponding functionResponse
|
||||||
toolID := genToolCallID()
|
toolID := genToolCallID()
|
||||||
pendingToolIDs = append(pendingToolIDs, toolID)
|
pendingToolIDs = append(pendingToolIDs, toolID)
|
||||||
toolUse, _ = sjson.Set(toolUse, "id", toolID)
|
toolUse, _ = sjson.SetBytes(toolUse, "id", toolID)
|
||||||
|
|
||||||
if name := fc.Get("name"); name.Exists() {
|
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() {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function response (from user) conversion to tool result
|
// Function response (from user) conversion to tool result
|
||||||
if fr := part.Get("functionResponse"); fr.Exists() {
|
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
|
// Attach the oldest queued tool_id to pair the response
|
||||||
// with its call. If the queue is empty, generate a new id.
|
// 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
|
// Fallback: generate new ID if no pending tool_use found
|
||||||
toolID = genToolCallID()
|
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
|
// Extract result content from the function response
|
||||||
if result := fr.Get("response.result"); result.Exists() {
|
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() {
|
} 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Image content (inline_data) conversion to Claude Code format
|
// Image content (inline_data) conversion to Claude Code format
|
||||||
if inlineData := part.Get("inline_data"); inlineData.Exists() {
|
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() {
|
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() {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// File data conversion to text content with file info
|
// File data conversion to text content with file info
|
||||||
if fileData := part.Get("file_data"); fileData.Exists() {
|
if fileData := part.Get("file_data"); fileData.Exists() {
|
||||||
// For file data, we'll convert to text content with file info
|
// 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()
|
fileInfo := "File: " + fileData.Get("file_uri").String()
|
||||||
if mimeType := fileData.Get("mime_type"); mimeType.Exists() {
|
if mimeType := fileData.Get("mime_type"); mimeType.Exists() {
|
||||||
fileInfo += " (Type: " + mimeType.String() + ")"
|
fileInfo += " (Type: " + mimeType.String() + ")"
|
||||||
}
|
}
|
||||||
textContent, _ = sjson.Set(textContent, "text", fileInfo)
|
textContent, _ = sjson.SetBytes(textContent, "text", fileInfo)
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", textContent)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", textContent)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,8 +336,8 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only add message if it has content
|
// Only add message if it has content
|
||||||
if contentArray := gjson.Get(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 {
|
if contentArray := gjson.GetBytes(msg, "content"); contentArray.Exists() && len(contentArray.Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -351,29 +351,29 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if funcDecls := tool.Get("functionDeclarations"); funcDecls.Exists() && funcDecls.IsArray() {
|
if funcDecls := tool.Get("functionDeclarations"); funcDecls.Exists() && funcDecls.IsArray() {
|
||||||
funcDecls.ForEach(func(_, funcDecl gjson.Result) bool {
|
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() {
|
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() {
|
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() {
|
if params := funcDecl.Get("parameters"); params.Exists() {
|
||||||
// Clean up the parameters schema for Claude Code compatibility
|
// Clean up the parameters schema for Claude Code compatibility
|
||||||
cleaned := params.Raw
|
cleaned := []byte(params.Raw)
|
||||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false)
|
||||||
cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
cleaned, _ = sjson.SetBytes(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
||||||
anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned)
|
anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned)
|
||||||
} else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() {
|
} else if params = funcDecl.Get("parametersJsonSchema"); params.Exists() {
|
||||||
// Clean up the parameters schema for Claude Code compatibility
|
// Clean up the parameters schema for Claude Code compatibility
|
||||||
cleaned := params.Raw
|
cleaned := []byte(params.Raw)
|
||||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false)
|
||||||
cleaned, _ = sjson.Set(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
cleaned, _ = sjson.SetBytes(cleaned, "$schema", "http://json-schema.org/draft-07/schema#")
|
||||||
anthropicTool, _ = sjson.SetRaw(anthropicTool, "input_schema", cleaned)
|
anthropicTool, _ = sjson.SetRawBytes(anthropicTool, "input_schema", cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
anthropicTools = append(anthropicTools, gjson.Parse(anthropicTool).Value())
|
anthropicTools = append(anthropicTools, gjson.ParseBytes(anthropicTool).Value())
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -381,7 +381,7 @@ func ConvertGeminiRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
})
|
})
|
||||||
|
|
||||||
if len(anthropicTools) > 0 {
|
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() {
|
if mode := funcCalling.Get("mode"); mode.Exists() {
|
||||||
switch mode.String() {
|
switch mode.String() {
|
||||||
case "AUTO":
|
case "AUTO":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`))
|
||||||
case "NONE":
|
case "NONE":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"none"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"none"}`))
|
||||||
case "ANY":
|
case "ANY":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream setting configuration
|
// 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
|
// Convert tool parameter types to lowercase for Claude Code compatibility
|
||||||
var pathsToLower []string
|
var pathsToLower []string
|
||||||
toolsResult := gjson.Get(out, "tools")
|
toolsResult := gjson.GetBytes(out, "tools")
|
||||||
util.Walk(toolsResult, "", "type", &pathsToLower)
|
util.Walk(toolsResult, "", "type", &pathsToLower)
|
||||||
for _, p := range pathsToLower {
|
for _, p := range pathsToLower {
|
||||||
fullPath := fmt.Sprintf("tools.%s", p)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -30,7 +30,7 @@ type ConvertAnthropicResponseToGeminiParams struct {
|
|||||||
Model string
|
Model string
|
||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
ResponseID string
|
ResponseID string
|
||||||
LastStorageOutput string
|
LastStorageOutput []byte
|
||||||
IsStreaming bool
|
IsStreaming bool
|
||||||
|
|
||||||
// Streaming state for tool_use assembly
|
// 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
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response
|
// - [][]byte: A slice of Gemini-compatible JSON responses
|
||||||
func ConvertClaudeResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertClaudeResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &ConvertAnthropicResponseToGeminiParams{
|
*param = &ConvertAnthropicResponseToGeminiParams{
|
||||||
Model: modelName,
|
Model: modelName,
|
||||||
@@ -63,7 +63,7 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
@@ -71,24 +71,24 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
eventType := root.Get("type").String()
|
eventType := root.Get("type").String()
|
||||||
|
|
||||||
// Base Gemini response template with default values
|
// 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
|
// Set model version
|
||||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).Model != "" {
|
if (*param).(*ConvertAnthropicResponseToGeminiParams).Model != "" {
|
||||||
// Map Claude model names back to Gemini model names
|
// 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
|
// Set response ID and creation time
|
||||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID != "" {
|
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
|
// Set creation time to current time if not provided
|
||||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt == 0 {
|
if (*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt == 0 {
|
||||||
(*param).(*ConvertAnthropicResponseToGeminiParams).CreatedAt = time.Now().Unix()
|
(*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 {
|
switch eventType {
|
||||||
case "message_start":
|
case "message_start":
|
||||||
@@ -97,7 +97,7 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
(*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID = message.Get("id").String()
|
(*param).(*ConvertAnthropicResponseToGeminiParams).ResponseID = message.Get("id").String()
|
||||||
(*param).(*ConvertAnthropicResponseToGeminiParams).Model = message.Get("model").String()
|
(*param).(*ConvertAnthropicResponseToGeminiParams).Model = message.Get("model").String()
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
case "content_block_start":
|
case "content_block_start":
|
||||||
// Start of a content block - record tool_use name by index for functionCall assembly
|
// 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":
|
case "content_block_delta":
|
||||||
// Handle content delta (text, thinking, or tool use arguments)
|
// Handle content delta (text, thinking, or tool use arguments)
|
||||||
@@ -123,16 +123,16 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
case "text_delta":
|
case "text_delta":
|
||||||
// Regular text content delta for normal response text
|
// Regular text content delta for normal response text
|
||||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||||
textPart := `{"text":""}`
|
textPart := []byte(`{"text":""}`)
|
||||||
textPart, _ = sjson.Set(textPart, "text", text.String())
|
textPart, _ = sjson.SetBytes(textPart, "text", text.String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", textPart)
|
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", textPart)
|
||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Thinking/reasoning content delta for models with reasoning capabilities
|
// Thinking/reasoning content delta for models with reasoning capabilities
|
||||||
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
||||||
thinkingPart := `{"thought":true,"text":""}`
|
thinkingPart := []byte(`{"thought":true,"text":""}`)
|
||||||
thinkingPart, _ = sjson.Set(thinkingPart, "text", text.String())
|
thinkingPart, _ = sjson.SetBytes(thinkingPart, "text", text.String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", thinkingPart)
|
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", thinkingPart)
|
||||||
}
|
}
|
||||||
case "input_json_delta":
|
case "input_json_delta":
|
||||||
// Tool use input delta - accumulate partial_json by index for later assembly at content_block_stop
|
// 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() {
|
if pj := delta.Get("partial_json"); pj.Exists() {
|
||||||
b.WriteString(pj.String())
|
b.WriteString(pj.String())
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
|
|
||||||
case "content_block_stop":
|
case "content_block_stop":
|
||||||
// End of content block - finalize tool calls if any
|
// End of content block - finalize tool calls if any
|
||||||
@@ -170,16 +170,16 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if name != "" || argsTrim != "" {
|
if name != "" || argsTrim != "" {
|
||||||
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
functionCall, _ = sjson.Set(functionCall, "functionCall.name", name)
|
functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", name)
|
||||||
}
|
}
|
||||||
if argsTrim != "" {
|
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.SetRawBytes(template, "candidates.0.content.parts.-1", functionCall)
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||||
(*param).(*ConvertAnthropicResponseToGeminiParams).LastStorageOutput = template
|
(*param).(*ConvertAnthropicResponseToGeminiParams).LastStorageOutput = append([]byte(nil), template...)
|
||||||
// cleanup used state for this index
|
// cleanup used state for this index
|
||||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs != nil {
|
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs != nil {
|
||||||
delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs, idx)
|
delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseArgs, idx)
|
||||||
@@ -187,9 +187,9 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames != nil {
|
if (*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames != nil {
|
||||||
delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames, idx)
|
delete((*param).(*ConvertAnthropicResponseToGeminiParams).ToolUseNames, idx)
|
||||||
}
|
}
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
case "message_delta":
|
case "message_delta":
|
||||||
// Handle message-level changes (like stop reason and usage information)
|
// 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() {
|
if stopReason := delta.Get("stop_reason"); stopReason.Exists() {
|
||||||
switch stopReason.String() {
|
switch stopReason.String() {
|
||||||
case "end_turn":
|
case "end_turn":
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||||
case "max_tokens":
|
case "max_tokens":
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "MAX_TOKENS")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "MAX_TOKENS")
|
||||||
case "stop_sequence":
|
case "stop_sequence":
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||||
default:
|
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()
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
|
|
||||||
// Set basic usage metadata according to Gemini API specification
|
// Set basic usage metadata according to Gemini API specification
|
||||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", inputTokens)
|
||||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
||||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", inputTokens+outputTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", inputTokens+outputTokens)
|
||||||
|
|
||||||
// Add cache-related token counts if present (Claude Code API cache fields)
|
// Add cache-related token counts if present (Claude Code API cache fields)
|
||||||
if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() {
|
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() {
|
if cacheReadTokens := usage.Get("cache_read_input_tokens"); cacheReadTokens.Exists() {
|
||||||
// Add cache read tokens to cached content count
|
// Add cache read tokens to cached content count
|
||||||
existingCacheTokens := usage.Get("cache_creation_input_tokens").Int()
|
existingCacheTokens := usage.Get("cache_creation_input_tokens").Int()
|
||||||
totalCacheTokens := existingCacheTokens + cacheReadTokens.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)
|
// Add thinking tokens if present (for models with reasoning capabilities)
|
||||||
if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() {
|
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)
|
// 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":
|
case "message_stop":
|
||||||
// Final message with usage information - no additional output needed
|
// Final message with usage information - no additional output needed
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
case "error":
|
case "error":
|
||||||
// Handle error responses and convert to Gemini error format
|
// Handle error responses and convert to Gemini error format
|
||||||
errorMsg := root.Get("error.message").String()
|
errorMsg := root.Get("error.message").String()
|
||||||
@@ -253,13 +253,13 @@ func ConvertClaudeResponseToGemini(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create error response in Gemini format
|
// Create error response in Gemini format
|
||||||
errorResponse := `{"error":{"code":400,"message":"","status":"INVALID_ARGUMENT"}}`
|
errorResponse := []byte(`{"error":{"code":400,"message":"","status":"INVALID_ARGUMENT"}}`)
|
||||||
errorResponse, _ = sjson.Set(errorResponse, "error.message", errorMsg)
|
errorResponse, _ = sjson.SetBytes(errorResponse, "error.message", errorMsg)
|
||||||
return []string{errorResponse}
|
return [][]byte{errorResponse}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Unknown event type, return empty response
|
// 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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response containing all message content and metadata
|
// - []byte: A Gemini-compatible JSON response containing all message content and metadata
|
||||||
func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
// Base Gemini response template for non-streaming with default values
|
// 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
|
// Set model version
|
||||||
template, _ = sjson.Set(template, "modelVersion", modelName)
|
template, _ = sjson.SetBytes(template, "modelVersion", modelName)
|
||||||
|
|
||||||
streamingEvents := make([][]byte, 0)
|
streamingEvents := make([][]byte, 0)
|
||||||
|
|
||||||
@@ -304,15 +304,15 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
Model: modelName,
|
Model: modelName,
|
||||||
CreatedAt: 0,
|
CreatedAt: 0,
|
||||||
ResponseID: "",
|
ResponseID: "",
|
||||||
LastStorageOutput: "",
|
LastStorageOutput: nil,
|
||||||
IsStreaming: false,
|
IsStreaming: false,
|
||||||
ToolUseNames: nil,
|
ToolUseNames: nil,
|
||||||
ToolUseArgs: nil,
|
ToolUseArgs: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process each streaming event and collect parts
|
// Process each streaming event and collect parts
|
||||||
var allParts []string
|
var allParts [][]byte
|
||||||
var finalUsageJSON string
|
var finalUsageJSON []byte
|
||||||
var responseID string
|
var responseID string
|
||||||
var createdAt int64
|
var createdAt int64
|
||||||
|
|
||||||
@@ -360,15 +360,15 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
case "text_delta":
|
case "text_delta":
|
||||||
// Process regular text content
|
// Process regular text content
|
||||||
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
if text := delta.Get("text"); text.Exists() && text.String() != "" {
|
||||||
partJSON := `{"text":""}`
|
partJSON := []byte(`{"text":""}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
partJSON, _ = sjson.SetBytes(partJSON, "text", text.String())
|
||||||
allParts = append(allParts, partJSON)
|
allParts = append(allParts, partJSON)
|
||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Process reasoning/thinking content
|
// Process reasoning/thinking content
|
||||||
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
if text := delta.Get("thinking"); text.Exists() && text.String() != "" {
|
||||||
partJSON := `{"thought":true,"text":""}`
|
partJSON := []byte(`{"thought":true,"text":""}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
partJSON, _ = sjson.SetBytes(partJSON, "text", text.String())
|
||||||
allParts = append(allParts, partJSON)
|
allParts = append(allParts, partJSON)
|
||||||
}
|
}
|
||||||
case "input_json_delta":
|
case "input_json_delta":
|
||||||
@@ -402,12 +402,12 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if name != "" || argsTrim != "" {
|
if name != "" || argsTrim != "" {
|
||||||
functionCallJSON := `{"functionCall":{"name":"","args":{}}}`
|
functionCallJSON := []byte(`{"functionCall":{"name":"","args":{}}}`)
|
||||||
if name != "" {
|
if name != "" {
|
||||||
functionCallJSON, _ = sjson.Set(functionCallJSON, "functionCall.name", name)
|
functionCallJSON, _ = sjson.SetBytes(functionCallJSON, "functionCall.name", name)
|
||||||
}
|
}
|
||||||
if argsTrim != "" {
|
if argsTrim != "" {
|
||||||
functionCallJSON, _ = sjson.SetRaw(functionCallJSON, "functionCall.args", argsTrim)
|
functionCallJSON, _ = sjson.SetRawBytes(functionCallJSON, "functionCall.args", []byte(argsTrim))
|
||||||
}
|
}
|
||||||
allParts = append(allParts, functionCallJSON)
|
allParts = append(allParts, functionCallJSON)
|
||||||
// cleanup used state for this index
|
// cleanup used state for this index
|
||||||
@@ -422,35 +422,35 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
case "message_delta":
|
case "message_delta":
|
||||||
// Extract final usage information using sjson for token counts and metadata
|
// Extract final usage information using sjson for token counts and metadata
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
usageJSON := `{}`
|
usageJSON := []byte(`{}`)
|
||||||
|
|
||||||
// Basic token counts for prompt and completion
|
// Basic token counts for prompt and completion
|
||||||
inputTokens := usage.Get("input_tokens").Int()
|
inputTokens := usage.Get("input_tokens").Int()
|
||||||
outputTokens := usage.Get("output_tokens").Int()
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
|
|
||||||
// Set basic usage metadata according to Gemini API specification
|
// Set basic usage metadata according to Gemini API specification
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "promptTokenCount", inputTokens)
|
usageJSON, _ = sjson.SetBytes(usageJSON, "promptTokenCount", inputTokens)
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "candidatesTokenCount", outputTokens)
|
usageJSON, _ = sjson.SetBytes(usageJSON, "candidatesTokenCount", outputTokens)
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "totalTokenCount", inputTokens+outputTokens)
|
usageJSON, _ = sjson.SetBytes(usageJSON, "totalTokenCount", inputTokens+outputTokens)
|
||||||
|
|
||||||
// Add cache-related token counts if present (Claude Code API cache fields)
|
// Add cache-related token counts if present (Claude Code API cache fields)
|
||||||
if cacheCreationTokens := usage.Get("cache_creation_input_tokens"); cacheCreationTokens.Exists() {
|
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() {
|
if cacheReadTokens := usage.Get("cache_read_input_tokens"); cacheReadTokens.Exists() {
|
||||||
// Add cache read tokens to cached content count
|
// Add cache read tokens to cached content count
|
||||||
existingCacheTokens := usage.Get("cache_creation_input_tokens").Int()
|
existingCacheTokens := usage.Get("cache_creation_input_tokens").Int()
|
||||||
totalCacheTokens := existingCacheTokens + cacheReadTokens.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)
|
// Add thinking tokens if present (for models with reasoning capabilities)
|
||||||
if thinkingTokens := usage.Get("thinking_tokens"); thinkingTokens.Exists() {
|
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)
|
// Set traffic type (required by Gemini API)
|
||||||
usageJSON, _ = sjson.Set(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
|
usageJSON, _ = sjson.SetBytes(usageJSON, "trafficType", "PROVISIONED_THROUGHPUT")
|
||||||
|
|
||||||
finalUsageJSON = usageJSON
|
finalUsageJSON = usageJSON
|
||||||
}
|
}
|
||||||
@@ -459,10 +459,10 @@ func ConvertClaudeResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
|
|
||||||
// Set response metadata
|
// Set response metadata
|
||||||
if responseID != "" {
|
if responseID != "" {
|
||||||
template, _ = sjson.Set(template, "responseId", responseID)
|
template, _ = sjson.SetBytes(template, "responseId", responseID)
|
||||||
}
|
}
|
||||||
if createdAt > 0 {
|
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
|
// 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
|
// Set the consolidated parts array
|
||||||
if len(consolidatedParts) > 0 {
|
if len(consolidatedParts) > 0 {
|
||||||
partsJSON := "[]"
|
partsJSON := []byte(`[]`)
|
||||||
for _, partJSON := range consolidatedParts {
|
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
|
// Set usage metadata
|
||||||
if finalUsageJSON != "" {
|
if len(finalUsageJSON) > 0 {
|
||||||
template, _ = sjson.SetRaw(template, "usageMetadata", finalUsageJSON)
|
template, _ = sjson.SetRawBytes(template, "usageMetadata", finalUsageJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
// consolidateParts merges consecutive text parts and thinking parts to create a cleaner response.
|
// 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
|
// 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.
|
// 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.
|
// 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 {
|
if len(parts) == 0 {
|
||||||
return parts
|
return parts
|
||||||
}
|
}
|
||||||
|
|
||||||
var consolidated []string
|
var consolidated [][]byte
|
||||||
var currentTextPart strings.Builder
|
var currentTextPart strings.Builder
|
||||||
var currentThoughtPart strings.Builder
|
var currentThoughtPart strings.Builder
|
||||||
var hasText, hasThought bool
|
var hasText, hasThought bool
|
||||||
@@ -506,8 +506,8 @@ func consolidateParts(parts []string) []string {
|
|||||||
flushText := func() {
|
flushText := func() {
|
||||||
// Flush accumulated text content to the consolidated parts array
|
// Flush accumulated text content to the consolidated parts array
|
||||||
if hasText && currentTextPart.Len() > 0 {
|
if hasText && currentTextPart.Len() > 0 {
|
||||||
textPartJSON := `{"text":""}`
|
textPartJSON := []byte(`{"text":""}`)
|
||||||
textPartJSON, _ = sjson.Set(textPartJSON, "text", currentTextPart.String())
|
textPartJSON, _ = sjson.SetBytes(textPartJSON, "text", currentTextPart.String())
|
||||||
consolidated = append(consolidated, textPartJSON)
|
consolidated = append(consolidated, textPartJSON)
|
||||||
currentTextPart.Reset()
|
currentTextPart.Reset()
|
||||||
hasText = false
|
hasText = false
|
||||||
@@ -517,8 +517,8 @@ func consolidateParts(parts []string) []string {
|
|||||||
flushThought := func() {
|
flushThought := func() {
|
||||||
// Flush accumulated thinking content to the consolidated parts array
|
// Flush accumulated thinking content to the consolidated parts array
|
||||||
if hasThought && currentThoughtPart.Len() > 0 {
|
if hasThought && currentThoughtPart.Len() > 0 {
|
||||||
thoughtPartJSON := `{"thought":true,"text":""}`
|
thoughtPartJSON := []byte(`{"thought":true,"text":""}`)
|
||||||
thoughtPartJSON, _ = sjson.Set(thoughtPartJSON, "text", currentThoughtPart.String())
|
thoughtPartJSON, _ = sjson.SetBytes(thoughtPartJSON, "text", currentThoughtPart.String())
|
||||||
consolidated = append(consolidated, thoughtPartJSON)
|
consolidated = append(consolidated, thoughtPartJSON)
|
||||||
currentThoughtPart.Reset()
|
currentThoughtPart.Reset()
|
||||||
hasThought = false
|
hasThought = false
|
||||||
@@ -526,7 +526,7 @@ func consolidateParts(parts []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, partJSON := range parts {
|
for _, partJSON := range parts {
|
||||||
part := gjson.Parse(partJSON)
|
part := gjson.ParseBytes(partJSON)
|
||||||
if !part.Exists() || !part.IsObject() {
|
if !part.Exists() || !part.IsObject() {
|
||||||
// Flush any pending parts and add this non-text part
|
// Flush any pending parts and add this non-text part
|
||||||
flushText()
|
flushText()
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
||||||
|
|
||||||
// Base Claude Code API template with default max_tokens value
|
// 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)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
@@ -79,20 +79,20 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
if supportsAdaptive {
|
if supportsAdaptive {
|
||||||
switch effort {
|
switch effort {
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.DeleteBytes(out, "output_config.effort")
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.DeleteBytes(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
||||||
effort = mapped
|
effort = mapped
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Set(out, "output_config.effort", effort)
|
out, _ = sjson.SetBytes(out, "output_config.effort", effort)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy/manual thinking (budget_tokens).
|
// Legacy/manual thinking (budget_tokens).
|
||||||
@@ -100,13 +100,13 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
if ok {
|
if ok {
|
||||||
switch budget {
|
switch budget {
|
||||||
case 0:
|
case 0:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
case -1:
|
case -1:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
default:
|
default:
|
||||||
if budget > 0 {
|
if budget > 0 {
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
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
|
// 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
|
// Max tokens configuration with fallback to default value
|
||||||
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
|
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
|
// Temperature setting for controlling response randomness
|
||||||
if temp := root.Get("temperature"); temp.Exists() {
|
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() {
|
} else if topP := root.Get("top_p"); topP.Exists() {
|
||||||
// Top P setting for nucleus sampling (filtered out if temperature is set)
|
// 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
|
// Stop sequences configuration for custom termination conditions
|
||||||
@@ -152,15 +152,15 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if len(stopSequences) > 0 {
|
if len(stopSequences) > 0 {
|
||||||
out, _ = sjson.Set(out, "stop_sequences", stopSequences)
|
out, _ = sjson.SetBytes(out, "stop_sequences", stopSequences)
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
// 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
|
// Process messages and transform them to Claude Code format
|
||||||
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
if messages := root.Get("messages"); messages.Exists() && messages.IsArray() {
|
||||||
@@ -173,39 +173,39 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
switch role {
|
switch role {
|
||||||
case "system":
|
case "system":
|
||||||
if systemMessageIndex == -1 {
|
if systemMessageIndex == -1 {
|
||||||
systemMsg := `{"role":"user","content":[]}`
|
systemMsg := []byte(`{"role":"user","content":[]}`)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", systemMsg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", systemMsg)
|
||||||
systemMessageIndex = messageIndex
|
systemMessageIndex = messageIndex
|
||||||
messageIndex++
|
messageIndex++
|
||||||
}
|
}
|
||||||
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
||||||
textPart := `{"type":"text","text":""}`
|
textPart := []byte(`{"type":"text","text":""}`)
|
||||||
textPart, _ = sjson.Set(textPart, "text", contentResult.String())
|
textPart, _ = sjson.SetBytes(textPart, "text", contentResult.String())
|
||||||
out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
|
out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
|
||||||
} else if contentResult.Exists() && contentResult.IsArray() {
|
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||||
if part.Get("type").String() == "text" {
|
if part.Get("type").String() == "text" {
|
||||||
textPart := `{"type":"text","text":""}`
|
textPart := []byte(`{"type":"text","text":""}`)
|
||||||
textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
|
textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())
|
||||||
out, _ = sjson.SetRaw(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
|
out, _ = sjson.SetRawBytes(out, fmt.Sprintf("messages.%d.content.-1", systemMessageIndex), textPart)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "user", "assistant":
|
case "user", "assistant":
|
||||||
msg := `{"role":"","content":[]}`
|
msg := []byte(`{"role":"","content":[]}`)
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||||
|
|
||||||
// Handle content based on its type (string or array)
|
// Handle content based on its type (string or array)
|
||||||
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
if contentResult.Exists() && contentResult.Type == gjson.String && contentResult.String() != "" {
|
||||||
part := `{"type":"text","text":""}`
|
part := []byte(`{"type":"text","text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", contentResult.String())
|
part, _ = sjson.SetBytes(part, "text", contentResult.String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", part)
|
||||||
} else if contentResult.Exists() && contentResult.IsArray() {
|
} else if contentResult.Exists() && contentResult.IsArray() {
|
||||||
contentResult.ForEach(func(_, part gjson.Result) bool {
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||||
claudePart := convertOpenAIContentPartToClaudePart(part)
|
claudePart := convertOpenAIContentPartToClaudePart(part)
|
||||||
if claudePart != "" {
|
if claudePart != "" {
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", claudePart)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", []byte(claudePart))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -221,9 +221,9 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
function := toolCall.Get("function")
|
function := toolCall.Get("function")
|
||||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolUse, _ = sjson.Set(toolUse, "id", toolCallID)
|
toolUse, _ = sjson.SetBytes(toolUse, "id", toolCallID)
|
||||||
toolUse, _ = sjson.Set(toolUse, "name", function.Get("name").String())
|
toolUse, _ = sjson.SetBytes(toolUse, "name", function.Get("name").String())
|
||||||
|
|
||||||
// Parse arguments for the tool call
|
// Parse arguments for the tool call
|
||||||
if args := function.Get("arguments"); args.Exists() {
|
if args := function.Get("arguments"); args.Exists() {
|
||||||
@@ -231,24 +231,24 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsJSON := gjson.Parse(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if argsJSON.IsObject() {
|
if argsJSON.IsObject() {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw))
|
||||||
} else {
|
} else {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}"))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte("{}"))
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||||
messageIndex++
|
messageIndex++
|
||||||
|
|
||||||
case "tool":
|
case "tool":
|
||||||
@@ -256,15 +256,15 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
toolCallID := message.Get("tool_call_id").String()
|
toolCallID := message.Get("tool_call_id").String()
|
||||||
toolContentResult := message.Get("content")
|
toolContentResult := message.Get("content")
|
||||||
|
|
||||||
msg := `{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`
|
msg := []byte(`{"role":"user","content":[{"type":"tool_result","tool_use_id":"","content":""}]}`)
|
||||||
msg, _ = sjson.Set(msg, "content.0.tool_use_id", toolCallID)
|
msg, _ = sjson.SetBytes(msg, "content.0.tool_use_id", toolCallID)
|
||||||
toolResultContent, toolResultContentRaw := convertOpenAIToolResultContent(toolContentResult)
|
toolResultContent, toolResultContentRaw := convertOpenAIToolResultContent(toolContentResult)
|
||||||
if toolResultContentRaw {
|
if toolResultContentRaw {
|
||||||
msg, _ = sjson.SetRaw(msg, "content.0.content", toolResultContent)
|
msg, _ = sjson.SetRawBytes(msg, "content.0.content", []byte(toolResultContent))
|
||||||
} else {
|
} 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++
|
messageIndex++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -277,25 +277,25 @@ func ConvertOpenAIRequestToClaude(modelName string, inputRawJSON []byte, stream
|
|||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if tool.Get("type").String() == "function" {
|
if tool.Get("type").String() == "function" {
|
||||||
function := tool.Get("function")
|
function := tool.Get("function")
|
||||||
anthropicTool := `{"name":"","description":""}`
|
anthropicTool := []byte(`{"name":"","description":""}`)
|
||||||
anthropicTool, _ = sjson.Set(anthropicTool, "name", function.Get("name").String())
|
anthropicTool, _ = sjson.SetBytes(anthropicTool, "name", function.Get("name").String())
|
||||||
anthropicTool, _ = sjson.Set(anthropicTool, "description", function.Get("description").String())
|
anthropicTool, _ = sjson.SetBytes(anthropicTool, "description", function.Get("description").String())
|
||||||
|
|
||||||
// Convert parameters schema for the tool
|
// Convert parameters schema for the tool
|
||||||
if parameters := function.Get("parameters"); parameters.Exists() {
|
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() {
|
} 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
|
hasAnthropicTools = true
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if !hasAnthropicTools {
|
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":
|
case "none":
|
||||||
// Don't set tool_choice, Claude Code will not use tools
|
// Don't set tool_choice, Claude Code will not use tools
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`))
|
||||||
case "required":
|
case "required":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`))
|
||||||
}
|
}
|
||||||
case gjson.JSON:
|
case gjson.JSON:
|
||||||
// Specific tool choice mapping
|
// Specific tool choice mapping
|
||||||
if toolChoice.Get("type").String() == "function" {
|
if toolChoice.Get("type").String() == "function" {
|
||||||
functionName := toolChoice.Get("function.name").String()
|
functionName := toolChoice.Get("function.name").String()
|
||||||
toolChoiceJSON := `{"type":"tool","name":""}`
|
toolChoiceJSON := []byte(`{"type":"tool","name":""}`)
|
||||||
toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", functionName)
|
toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "name", functionName)
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(out)
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertOpenAIContentPartToClaudePart(part gjson.Result) string {
|
func convertOpenAIContentPartToClaudePart(part gjson.Result) string {
|
||||||
switch part.Get("type").String() {
|
switch part.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
textPart := `{"type":"text","text":""}`
|
textPart := []byte(`{"type":"text","text":""}`)
|
||||||
textPart, _ = sjson.Set(textPart, "text", part.Get("text").String())
|
textPart, _ = sjson.SetBytes(textPart, "text", part.Get("text").String())
|
||||||
return textPart
|
return string(textPart)
|
||||||
|
|
||||||
case "image_url":
|
case "image_url":
|
||||||
return convertOpenAIImageURLToClaudePart(part.Get("image_url.url").String())
|
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 {
|
if semicolonIdx != -1 && commaIdx != -1 && commaIdx > semicolonIdx {
|
||||||
mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:")
|
mediaType := strings.TrimPrefix(fileData[:semicolonIdx], "data:")
|
||||||
data := fileData[commaIdx+1:]
|
data := fileData[commaIdx+1:]
|
||||||
docPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}`
|
docPart := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`)
|
||||||
docPart, _ = sjson.Set(docPart, "source.media_type", mediaType)
|
docPart, _ = sjson.SetBytes(docPart, "source.media_type", mediaType)
|
||||||
docPart, _ = sjson.Set(docPart, "source.data", data)
|
docPart, _ = sjson.SetBytes(docPart, "source.data", data)
|
||||||
return docPart
|
return string(docPart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,15 +373,15 @@ func convertOpenAIImageURLToClaudePart(imageURL string) string {
|
|||||||
mediaType = "application/octet-stream"
|
mediaType = "application/octet-stream"
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePart := `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
imagePart := []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`)
|
||||||
imagePart, _ = sjson.Set(imagePart, "source.media_type", mediaType)
|
imagePart, _ = sjson.SetBytes(imagePart, "source.media_type", mediaType)
|
||||||
imagePart, _ = sjson.Set(imagePart, "source.data", parts[1])
|
imagePart, _ = sjson.SetBytes(imagePart, "source.data", parts[1])
|
||||||
return imagePart
|
return string(imagePart)
|
||||||
}
|
}
|
||||||
|
|
||||||
imagePart := `{"type":"image","source":{"type":"url","url":""}}`
|
imagePart := []byte(`{"type":"image","source":{"type":"url","url":""}}`)
|
||||||
imagePart, _ = sjson.Set(imagePart, "source.url", imageURL)
|
imagePart, _ = sjson.SetBytes(imagePart, "source.url", imageURL)
|
||||||
return imagePart
|
return string(imagePart)
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertOpenAIToolResultContent(content gjson.Result) (string, bool) {
|
func convertOpenAIToolResultContent(content gjson.Result) (string, bool) {
|
||||||
@@ -394,28 +394,28 @@ func convertOpenAIToolResultContent(content gjson.Result) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if content.IsArray() {
|
if content.IsArray() {
|
||||||
claudeContent := "[]"
|
claudeContent := []byte("[]")
|
||||||
partCount := 0
|
partCount := 0
|
||||||
|
|
||||||
content.ForEach(func(_, part gjson.Result) bool {
|
content.ForEach(func(_, part gjson.Result) bool {
|
||||||
if part.Type == gjson.String {
|
if part.Type == gjson.String {
|
||||||
textPart := `{"type":"text","text":""}`
|
textPart := []byte(`{"type":"text","text":""}`)
|
||||||
textPart, _ = sjson.Set(textPart, "text", part.String())
|
textPart, _ = sjson.SetBytes(textPart, "text", part.String())
|
||||||
claudeContent, _ = sjson.SetRaw(claudeContent, "-1", textPart)
|
claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", textPart)
|
||||||
partCount++
|
partCount++
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
claudePart := convertOpenAIContentPartToClaudePart(part)
|
claudePart := convertOpenAIContentPartToClaudePart(part)
|
||||||
if claudePart != "" {
|
if claudePart != "" {
|
||||||
claudeContent, _ = sjson.SetRaw(claudeContent, "-1", claudePart)
|
claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", []byte(claudePart))
|
||||||
partCount++
|
partCount++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if partCount > 0 || len(content.Array()) == 0 {
|
if partCount > 0 || len(content.Array()) == 0 {
|
||||||
return claudeContent, true
|
return string(claudeContent), true
|
||||||
}
|
}
|
||||||
|
|
||||||
return content.Raw, false
|
return content.Raw, false
|
||||||
@@ -424,9 +424,9 @@ func convertOpenAIToolResultContent(content gjson.Result) (string, bool) {
|
|||||||
if content.IsObject() {
|
if content.IsObject() {
|
||||||
claudePart := convertOpenAIContentPartToClaudePart(content)
|
claudePart := convertOpenAIContentPartToClaudePart(content)
|
||||||
if claudePart != "" {
|
if claudePart != "" {
|
||||||
claudeContent := "[]"
|
claudeContent := []byte("[]")
|
||||||
claudeContent, _ = sjson.SetRaw(claudeContent, "-1", claudePart)
|
claudeContent, _ = sjson.SetRawBytes(claudeContent, "-1", []byte(claudePart))
|
||||||
return claudeContent, true
|
return string(claudeContent), true
|
||||||
}
|
}
|
||||||
return content.Raw, false
|
return content.Raw, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ type ToolCallAccumulator struct {
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
// - [][]byte: A slice of OpenAI-compatible JSON responses
|
||||||
func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
var localParam any
|
var localParam any
|
||||||
if param == nil {
|
if param == nil {
|
||||||
param = &localParam
|
param = &localParam
|
||||||
@@ -63,7 +63,7 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
@@ -71,19 +71,19 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
eventType := root.Get("type").String()
|
eventType := root.Get("type").String()
|
||||||
|
|
||||||
// Base OpenAI streaming response template
|
// 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
|
// Set model
|
||||||
if modelName != "" {
|
if modelName != "" {
|
||||||
template, _ = sjson.Set(template, "model", modelName)
|
template, _ = sjson.SetBytes(template, "model", modelName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set response ID and creation time
|
// Set response ID and creation time
|
||||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID != "" {
|
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 {
|
if (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt > 0 {
|
||||||
template, _ = sjson.Set(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt)
|
template, _ = sjson.SetBytes(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch eventType {
|
switch eventType {
|
||||||
@@ -93,19 +93,19 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID = message.Get("id").String()
|
(*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID = message.Get("id").String()
|
||||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt = time.Now().Unix()
|
(*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt = time.Now().Unix()
|
||||||
|
|
||||||
template, _ = sjson.Set(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID)
|
template, _ = sjson.SetBytes(template, "id", (*param).(*ConvertAnthropicResponseToOpenAIParams).ResponseID)
|
||||||
template, _ = sjson.Set(template, "model", modelName)
|
template, _ = sjson.SetBytes(template, "model", modelName)
|
||||||
template, _ = sjson.Set(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt)
|
template, _ = sjson.SetBytes(template, "created", (*param).(*ConvertAnthropicResponseToOpenAIParams).CreatedAt)
|
||||||
|
|
||||||
// Set initial role to assistant for the response
|
// 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
|
// Initialize tool calls accumulator for tracking tool call progress
|
||||||
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator == nil {
|
if (*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator == nil {
|
||||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
(*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator = make(map[int]*ToolCallAccumulator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
|
|
||||||
case "content_block_start":
|
case "content_block_start":
|
||||||
// Start of a content block (text, tool use, or reasoning)
|
// Start of a content block (text, tool use, or reasoning)
|
||||||
@@ -128,10 +128,10 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't output anything yet - wait for complete tool call
|
// Don't output anything yet - wait for complete tool call
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
case "content_block_delta":
|
case "content_block_delta":
|
||||||
// Handle content delta (text, tool use arguments, or reasoning content)
|
// Handle content delta (text, tool use arguments, or reasoning content)
|
||||||
@@ -143,13 +143,13 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
case "text_delta":
|
case "text_delta":
|
||||||
// Text content delta - send incremental text updates
|
// Text content delta - send incremental text updates
|
||||||
if text := delta.Get("text"); text.Exists() {
|
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
|
hasContent = true
|
||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Accumulate reasoning/thinking content
|
// Accumulate reasoning/thinking content
|
||||||
if thinking := delta.Get("thinking"); thinking.Exists() {
|
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
|
hasContent = true
|
||||||
}
|
}
|
||||||
case "input_json_delta":
|
case "input_json_delta":
|
||||||
@@ -163,13 +163,13 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Don't output anything yet - wait for complete tool call
|
// Don't output anything yet - wait for complete tool call
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasContent {
|
if hasContent {
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
} else {
|
} else {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
case "content_block_stop":
|
case "content_block_stop":
|
||||||
@@ -182,26 +182,26 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
if arguments == "" {
|
if arguments == "" {
|
||||||
arguments = "{}"
|
arguments = "{}"
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.index", index)
|
template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.index", index)
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.id", accumulator.ID)
|
template, _ = sjson.SetBytes(template, "choices.0.delta.tool_calls.0.id", accumulator.ID)
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.tool_calls.0.type", "function")
|
template, _ = sjson.SetBytes(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.SetBytes(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.function.arguments", arguments)
|
||||||
|
|
||||||
// Clean up the accumulator for this index
|
// Clean up the accumulator for this index
|
||||||
delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
|
delete((*param).(*ConvertAnthropicResponseToOpenAIParams).ToolCallsAccumulator, index)
|
||||||
|
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
case "message_delta":
|
case "message_delta":
|
||||||
// Handle message-level changes including stop reason and usage
|
// Handle message-level changes including stop reason and usage
|
||||||
if delta := root.Get("delta"); delta.Exists() {
|
if delta := root.Get("delta"); delta.Exists() {
|
||||||
if stopReason := delta.Get("stop_reason"); stopReason.Exists() {
|
if stopReason := delta.Get("stop_reason"); stopReason.Exists() {
|
||||||
(*param).(*ConvertAnthropicResponseToOpenAIParams).FinishReason = mapAnthropicStopReasonToOpenAI(stopReason.String())
|
(*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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,34 +211,34 @@ func ConvertClaudeResponseToOpenAI(_ context.Context, modelName string, original
|
|||||||
outputTokens := usage.Get("output_tokens").Int()
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
cacheReadInputTokens := usage.Get("cache_read_input_tokens").Int()
|
cacheReadInputTokens := usage.Get("cache_read_input_tokens").Int()
|
||||||
cacheCreationInputTokens := usage.Get("cache_creation_input_tokens").Int()
|
cacheCreationInputTokens := usage.Get("cache_creation_input_tokens").Int()
|
||||||
template, _ = sjson.Set(template, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens)
|
template, _ = sjson.SetBytes(template, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens)
|
||||||
template, _ = sjson.Set(template, "usage.completion_tokens", outputTokens)
|
template, _ = sjson.SetBytes(template, "usage.completion_tokens", outputTokens)
|
||||||
template, _ = sjson.Set(template, "usage.total_tokens", inputTokens+outputTokens)
|
template, _ = sjson.SetBytes(template, "usage.total_tokens", inputTokens+outputTokens)
|
||||||
template, _ = sjson.Set(template, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens)
|
template, _ = sjson.SetBytes(template, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens)
|
||||||
}
|
}
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
|
|
||||||
case "message_stop":
|
case "message_stop":
|
||||||
// Final message event - no additional output needed
|
// Final message event - no additional output needed
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
case "ping":
|
case "ping":
|
||||||
// Ping events for keeping connection alive - no output needed
|
// Ping events for keeping connection alive - no output needed
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
case "error":
|
case "error":
|
||||||
// Error event - format and return error response
|
// Error event - format and return error response
|
||||||
if errorData := root.Get("error"); errorData.Exists() {
|
if errorData := root.Get("error"); errorData.Exists() {
|
||||||
errorJSON := `{"error":{"message":"","type":""}}`
|
errorJSON := []byte(`{"error":{"message":"","type":""}}`)
|
||||||
errorJSON, _ = sjson.Set(errorJSON, "error.message", errorData.Get("message").String())
|
errorJSON, _ = sjson.SetBytes(errorJSON, "error.message", errorData.Get("message").String())
|
||||||
errorJSON, _ = sjson.Set(errorJSON, "error.type", errorData.Get("type").String())
|
errorJSON, _ = sjson.SetBytes(errorJSON, "error.type", errorData.Get("type").String())
|
||||||
return []string{errorJSON}
|
return [][]byte{errorJSON}
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Unknown event type - ignore
|
// Unknown event type - ignore
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,8 +270,8 @@ func mapAnthropicStopReasonToOpenAI(anthropicReason string) string {
|
|||||||
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []byte: An OpenAI-compatible JSON response containing all message content and metadata
|
||||||
func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
chunks := make([][]byte, 0)
|
chunks := make([][]byte, 0)
|
||||||
|
|
||||||
lines := bytes.Split(rawJSON, []byte("\n"))
|
lines := bytes.Split(rawJSON, []byte("\n"))
|
||||||
@@ -283,7 +283,7 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Base OpenAI non-streaming response template
|
// 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 messageID string
|
||||||
var model string
|
var model string
|
||||||
@@ -370,28 +370,28 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
outputTokens := usage.Get("output_tokens").Int()
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
cacheReadInputTokens := usage.Get("cache_read_input_tokens").Int()
|
cacheReadInputTokens := usage.Get("cache_read_input_tokens").Int()
|
||||||
cacheCreationInputTokens := usage.Get("cache_creation_input_tokens").Int()
|
cacheCreationInputTokens := usage.Get("cache_creation_input_tokens").Int()
|
||||||
out, _ = sjson.Set(out, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens)
|
out, _ = sjson.SetBytes(out, "usage.prompt_tokens", inputTokens+cacheCreationInputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.completion_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.completion_tokens", outputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.total_tokens", inputTokens+outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.total_tokens", inputTokens+outputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens)
|
out, _ = sjson.SetBytes(out, "usage.prompt_tokens_details.cached_tokens", cacheReadInputTokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set basic response fields including message ID, creation time, and model
|
// Set basic response fields including message ID, creation time, and model
|
||||||
out, _ = sjson.Set(out, "id", messageID)
|
out, _ = sjson.SetBytes(out, "id", messageID)
|
||||||
out, _ = sjson.Set(out, "created", createdAt)
|
out, _ = sjson.SetBytes(out, "created", createdAt)
|
||||||
out, _ = sjson.Set(out, "model", model)
|
out, _ = sjson.SetBytes(out, "model", model)
|
||||||
|
|
||||||
// Set message content by combining all text parts
|
// Set message content by combining all text parts
|
||||||
messageContent := strings.Join(contentParts, "")
|
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)
|
// Add reasoning content if available (following OpenAI reasoning format)
|
||||||
if len(reasoningParts) > 0 {
|
if len(reasoningParts) > 0 {
|
||||||
reasoningContent := strings.Join(reasoningParts, "")
|
reasoningContent := strings.Join(reasoningParts, "")
|
||||||
// Add reasoning as a separate field in the message
|
// 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
|
// Set tool calls if any were accumulated during processing
|
||||||
@@ -417,19 +417,19 @@ func ConvertClaudeResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount)
|
namePath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.name", toolCallsCount)
|
||||||
argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount)
|
argumentsPath := fmt.Sprintf("choices.0.message.tool_calls.%d.function.arguments", toolCallsCount)
|
||||||
|
|
||||||
out, _ = sjson.Set(out, idPath, accumulator.ID)
|
out, _ = sjson.SetBytes(out, idPath, accumulator.ID)
|
||||||
out, _ = sjson.Set(out, typePath, "function")
|
out, _ = sjson.SetBytes(out, typePath, "function")
|
||||||
out, _ = sjson.Set(out, namePath, accumulator.Name)
|
out, _ = sjson.SetBytes(out, namePath, accumulator.Name)
|
||||||
out, _ = sjson.Set(out, argumentsPath, arguments)
|
out, _ = sjson.SetBytes(out, argumentsPath, arguments)
|
||||||
toolCallsCount++
|
toolCallsCount++
|
||||||
}
|
}
|
||||||
if toolCallsCount > 0 {
|
if toolCallsCount > 0 {
|
||||||
out, _ = sjson.Set(out, "choices.0.finish_reason", "tool_calls")
|
out, _ = sjson.SetBytes(out, "choices.0.finish_reason", "tool_calls")
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
out, _ = sjson.SetBytes(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
out, _ = sjson.SetBytes(out, "choices.0.finish_reason", mapAnthropicStopReasonToOpenAI(stopReason))
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
userID := fmt.Sprintf("user_%s_account_%s_session_%s", user, account, session)
|
||||||
|
|
||||||
// Base Claude message payload
|
// 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)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
@@ -67,20 +67,20 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
if supportsAdaptive {
|
if supportsAdaptive {
|
||||||
switch effort {
|
switch effort {
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.DeleteBytes(out, "output_config.effort")
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Delete(out, "output_config.effort")
|
out, _ = sjson.DeleteBytes(out, "output_config.effort")
|
||||||
default:
|
default:
|
||||||
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
if mapped, ok := thinking.MapToClaudeEffort(effort, supportsMax); ok {
|
||||||
effort = mapped
|
effort = mapped
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, "thinking.type", "adaptive")
|
out, _ = sjson.SetBytes(out, "thinking.type", "adaptive")
|
||||||
out, _ = sjson.Delete(out, "thinking.budget_tokens")
|
out, _ = sjson.DeleteBytes(out, "thinking.budget_tokens")
|
||||||
out, _ = sjson.Set(out, "output_config.effort", effort)
|
out, _ = sjson.SetBytes(out, "output_config.effort", effort)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy/manual thinking (budget_tokens).
|
// Legacy/manual thinking (budget_tokens).
|
||||||
@@ -88,13 +88,13 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
if ok {
|
if ok {
|
||||||
switch budget {
|
switch budget {
|
||||||
case 0:
|
case 0:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "disabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "disabled")
|
||||||
case -1:
|
case -1:
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
default:
|
default:
|
||||||
if budget > 0 {
|
if budget > 0 {
|
||||||
out, _ = sjson.Set(out, "thinking.type", "enabled")
|
out, _ = sjson.SetBytes(out, "thinking.type", "enabled")
|
||||||
out, _ = sjson.Set(out, "thinking.budget_tokens", budget)
|
out, _ = sjson.SetBytes(out, "thinking.budget_tokens", budget)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,15 +114,15 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model
|
// Model
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// Max tokens
|
// Max tokens
|
||||||
if mot := root.Get("max_output_tokens"); mot.Exists() {
|
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
|
// 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)
|
// instructions -> as a leading message (use role user for Claude API compatibility)
|
||||||
instructionsText := ""
|
instructionsText := ""
|
||||||
@@ -130,9 +130,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String {
|
if instr := root.Get("instructions"); instr.Exists() && instr.Type == gjson.String {
|
||||||
instructionsText = instr.String()
|
instructionsText = instr.String()
|
||||||
if instructionsText != "" {
|
if instructionsText != "" {
|
||||||
sysMsg := `{"role":"user","content":""}`
|
sysMsg := []byte(`{"role":"user","content":""}`)
|
||||||
sysMsg, _ = sjson.Set(sysMsg, "content", instructionsText)
|
sysMsg, _ = sjson.SetBytes(sysMsg, "content", instructionsText)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", sysMsg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", sysMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,9 +156,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
}
|
}
|
||||||
instructionsText = builder.String()
|
instructionsText = builder.String()
|
||||||
if instructionsText != "" {
|
if instructionsText != "" {
|
||||||
sysMsg := `{"role":"user","content":""}`
|
sysMsg := []byte(`{"role":"user","content":""}`)
|
||||||
sysMsg, _ = sjson.Set(sysMsg, "content", instructionsText)
|
sysMsg, _ = sjson.SetBytes(sysMsg, "content", instructionsText)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", sysMsg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", sysMsg)
|
||||||
extractedFromSystem = true
|
extractedFromSystem = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,9 +193,9 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
if t := part.Get("text"); t.Exists() {
|
if t := part.Get("text"); t.Exists() {
|
||||||
txt := t.String()
|
txt := t.String()
|
||||||
textAggregate.WriteString(txt)
|
textAggregate.WriteString(txt)
|
||||||
contentPart := `{"type":"text","text":""}`
|
contentPart := []byte(`{"type":"text","text":""}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "text", txt)
|
contentPart, _ = sjson.SetBytes(contentPart, "text", txt)
|
||||||
partsJSON = append(partsJSON, contentPart)
|
partsJSON = append(partsJSON, string(contentPart))
|
||||||
}
|
}
|
||||||
if ptype == "input_text" {
|
if ptype == "input_text" {
|
||||||
role = "user"
|
role = "user"
|
||||||
@@ -208,7 +208,7 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
url = part.Get("url").String()
|
url = part.Get("url").String()
|
||||||
}
|
}
|
||||||
if url != "" {
|
if url != "" {
|
||||||
var contentPart string
|
var contentPart []byte
|
||||||
if strings.HasPrefix(url, "data:") {
|
if strings.HasPrefix(url, "data:") {
|
||||||
trimmed := strings.TrimPrefix(url, "data:")
|
trimmed := strings.TrimPrefix(url, "data:")
|
||||||
mediaAndData := strings.SplitN(trimmed, ";base64,", 2)
|
mediaAndData := strings.SplitN(trimmed, ";base64,", 2)
|
||||||
@@ -221,16 +221,16 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
data = mediaAndData[1]
|
data = mediaAndData[1]
|
||||||
}
|
}
|
||||||
if data != "" {
|
if data != "" {
|
||||||
contentPart = `{"type":"image","source":{"type":"base64","media_type":"","data":""}}`
|
contentPart = []byte(`{"type":"image","source":{"type":"base64","media_type":"","data":""}}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType)
|
contentPart, _ = sjson.SetBytes(contentPart, "source.media_type", mediaType)
|
||||||
contentPart, _ = sjson.Set(contentPart, "source.data", data)
|
contentPart, _ = sjson.SetBytes(contentPart, "source.data", data)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentPart = `{"type":"image","source":{"type":"url","url":""}}`
|
contentPart = []byte(`{"type":"image","source":{"type":"url","url":""}}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "source.url", url)
|
contentPart, _ = sjson.SetBytes(contentPart, "source.url", url)
|
||||||
}
|
}
|
||||||
if contentPart != "" {
|
if len(contentPart) > 0 {
|
||||||
partsJSON = append(partsJSON, contentPart)
|
partsJSON = append(partsJSON, string(contentPart))
|
||||||
if role == "" {
|
if role == "" {
|
||||||
role = "user"
|
role = "user"
|
||||||
}
|
}
|
||||||
@@ -252,10 +252,10 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
data = mediaAndData[1]
|
data = mediaAndData[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentPart := `{"type":"document","source":{"type":"base64","media_type":"","data":""}}`
|
contentPart := []byte(`{"type":"document","source":{"type":"base64","media_type":"","data":""}}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "source.media_type", mediaType)
|
contentPart, _ = sjson.SetBytes(contentPart, "source.media_type", mediaType)
|
||||||
contentPart, _ = sjson.Set(contentPart, "source.data", data)
|
contentPart, _ = sjson.SetBytes(contentPart, "source.data", data)
|
||||||
partsJSON = append(partsJSON, contentPart)
|
partsJSON = append(partsJSON, string(contentPart))
|
||||||
if role == "" {
|
if role == "" {
|
||||||
role = "user"
|
role = "user"
|
||||||
}
|
}
|
||||||
@@ -280,24 +280,24 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(partsJSON) > 0 {
|
if len(partsJSON) > 0 {
|
||||||
msg := `{"role":"","content":[]}`
|
msg := []byte(`{"role":"","content":[]}`)
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||||
if len(partsJSON) == 1 && !hasImage && !hasFile {
|
if len(partsJSON) == 1 && !hasImage && !hasFile {
|
||||||
// Preserve legacy behavior for single text content
|
// Preserve legacy behavior for single text content
|
||||||
msg, _ = sjson.Delete(msg, "content")
|
msg, _ = sjson.DeleteBytes(msg, "content")
|
||||||
textPart := gjson.Parse(partsJSON[0])
|
textPart := gjson.Parse(partsJSON[0])
|
||||||
msg, _ = sjson.Set(msg, "content", textPart.Get("text").String())
|
msg, _ = sjson.SetBytes(msg, "content", textPart.Get("text").String())
|
||||||
} else {
|
} else {
|
||||||
for _, partJSON := range partsJSON {
|
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" {
|
} else if textAggregate.Len() > 0 || role == "system" {
|
||||||
msg := `{"role":"","content":""}`
|
msg := []byte(`{"role":"","content":""}`)
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||||
msg, _ = sjson.Set(msg, "content", textAggregate.String())
|
msg, _ = sjson.SetBytes(msg, "content", textAggregate.String())
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "function_call":
|
case "function_call":
|
||||||
@@ -309,31 +309,31 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
name := item.Get("name").String()
|
name := item.Get("name").String()
|
||||||
argsStr := item.Get("arguments").String()
|
argsStr := item.Get("arguments").String()
|
||||||
|
|
||||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolUse, _ = sjson.Set(toolUse, "id", callID)
|
toolUse, _ = sjson.SetBytes(toolUse, "id", callID)
|
||||||
toolUse, _ = sjson.Set(toolUse, "name", name)
|
toolUse, _ = sjson.SetBytes(toolUse, "name", name)
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsJSON := gjson.Parse(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if argsJSON.IsObject() {
|
if argsJSON.IsObject() {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
asst := `{"role":"assistant","content":[]}`
|
asst := []byte(`{"role":"assistant","content":[]}`)
|
||||||
asst, _ = sjson.SetRaw(asst, "content.-1", toolUse)
|
asst, _ = sjson.SetRawBytes(asst, "content.-1", toolUse)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", asst)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", asst)
|
||||||
|
|
||||||
case "function_call_output":
|
case "function_call_output":
|
||||||
// Map to user tool_result
|
// Map to user tool_result
|
||||||
callID := item.Get("call_id").String()
|
callID := item.Get("call_id").String()
|
||||||
outputStr := item.Get("output").String()
|
outputStr := item.Get("output").String()
|
||||||
toolResult := `{"type":"tool_result","tool_use_id":"","content":""}`
|
toolResult := []byte(`{"type":"tool_result","tool_use_id":"","content":""}`)
|
||||||
toolResult, _ = sjson.Set(toolResult, "tool_use_id", callID)
|
toolResult, _ = sjson.SetBytes(toolResult, "tool_use_id", callID)
|
||||||
toolResult, _ = sjson.Set(toolResult, "content", outputStr)
|
toolResult, _ = sjson.SetBytes(toolResult, "content", outputStr)
|
||||||
|
|
||||||
usr := `{"role":"user","content":[]}`
|
usr := []byte(`{"role":"user","content":[]}`)
|
||||||
usr, _ = sjson.SetRaw(usr, "content.-1", toolResult)
|
usr, _ = sjson.SetRawBytes(usr, "content.-1", toolResult)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", usr)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", usr)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -341,27 +341,27 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
|
|
||||||
// tools mapping: parameters -> input_schema
|
// tools mapping: parameters -> input_schema
|
||||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||||
toolsJSON := "[]"
|
toolsJSON := []byte("[]")
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
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() {
|
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() {
|
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() {
|
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() {
|
} 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
|
return true
|
||||||
})
|
})
|
||||||
if gjson.Parse(toolsJSON).IsArray() && len(gjson.Parse(toolsJSON).Array()) > 0 {
|
if parsedTools := gjson.ParseBytes(toolsJSON); parsedTools.IsArray() && len(parsedTools.Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "tools", toolsJSON)
|
out, _ = sjson.SetRawBytes(out, "tools", toolsJSON)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,23 +371,23 @@ func ConvertOpenAIResponsesRequestToClaude(modelName string, inputRawJSON []byte
|
|||||||
case gjson.String:
|
case gjson.String:
|
||||||
switch toolChoice.String() {
|
switch toolChoice.String() {
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"auto"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"auto"}`))
|
||||||
case "none":
|
case "none":
|
||||||
// Leave unset; implies no tools
|
// Leave unset; implies no tools
|
||||||
case "required":
|
case "required":
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", `{"type":"any"}`)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", []byte(`{"type":"any"}`))
|
||||||
}
|
}
|
||||||
case gjson.JSON:
|
case gjson.JSON:
|
||||||
if toolChoice.Get("type").String() == "function" {
|
if toolChoice.Get("type").String() == "function" {
|
||||||
fn := toolChoice.Get("function.name").String()
|
fn := toolChoice.Get("function.name").String()
|
||||||
toolChoiceJSON := `{"name":"","type":"tool"}`
|
toolChoiceJSON := []byte(`{"name":"","type":"tool"}`)
|
||||||
toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "name", fn)
|
toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "name", fn)
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(out)
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -50,12 +51,12 @@ func pickRequestJSON(originalRequestRawJSON, requestRawJSON []byte) []byte {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func emitEvent(event string, payload string) string {
|
func emitEvent(event string, payload []byte) []byte {
|
||||||
return fmt.Sprintf("event: %s\ndata: %s", event, payload)
|
return translatorcommon.SSEEventData(event, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertClaudeResponseToOpenAIResponses converts Claude SSE to OpenAI Responses SSE events.
|
// 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 {
|
if *param == nil {
|
||||||
*param = &claudeToResponsesState{FuncArgsBuf: make(map[int]*strings.Builder), FuncNames: make(map[int]string), FuncCallIDs: make(map[int]string)}
|
*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
|
// Expect `data: {..}` from Claude clients
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
ev := root.Get("type").String()
|
ev := root.Get("type").String()
|
||||||
var out []string
|
var out [][]byte
|
||||||
|
|
||||||
nextSeq := func() int { st.Seq++; return st.Seq }
|
nextSeq := func() int { st.Seq++; return st.Seq }
|
||||||
|
|
||||||
@@ -105,16 +106,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// response.created
|
// 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 := []byte(`{"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.SetBytes(created, "sequence_number", nextSeq())
|
||||||
created, _ = sjson.Set(created, "response.id", st.ResponseID)
|
created, _ = sjson.SetBytes(created, "response.id", st.ResponseID)
|
||||||
created, _ = sjson.Set(created, "response.created_at", st.CreatedAt)
|
created, _ = sjson.SetBytes(created, "response.created_at", st.CreatedAt)
|
||||||
out = append(out, emitEvent("response.created", created))
|
out = append(out, emitEvent("response.created", created))
|
||||||
// response.in_progress
|
// response.in_progress
|
||||||
inprog := `{"type":"response.in_progress","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"in_progress"}}`
|
inprog := []byte(`{"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.SetBytes(inprog, "sequence_number", nextSeq())
|
||||||
inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID)
|
inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID)
|
||||||
inprog, _ = sjson.Set(inprog, "response.created_at", st.CreatedAt)
|
inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.CreatedAt)
|
||||||
out = append(out, emitEvent("response.in_progress", inprog))
|
out = append(out, emitEvent("response.in_progress", inprog))
|
||||||
}
|
}
|
||||||
case "content_block_start":
|
case "content_block_start":
|
||||||
@@ -128,25 +129,25 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
// open message item + content part
|
// open message item + content part
|
||||||
st.InTextBlock = true
|
st.InTextBlock = true
|
||||||
st.CurrentMsgID = fmt.Sprintf("msg_%s_0", st.ResponseID)
|
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 := []byte(`{"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.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "item.id", st.CurrentMsgID)
|
item, _ = sjson.SetBytes(item, "item.id", st.CurrentMsgID)
|
||||||
out = append(out, emitEvent("response.output_item.added", item))
|
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 := []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.Set(part, "sequence_number", nextSeq())
|
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
|
||||||
part, _ = sjson.Set(part, "item_id", st.CurrentMsgID)
|
part, _ = sjson.SetBytes(part, "item_id", st.CurrentMsgID)
|
||||||
out = append(out, emitEvent("response.content_part.added", part))
|
out = append(out, emitEvent("response.content_part.added", part))
|
||||||
} else if typ == "tool_use" {
|
} else if typ == "tool_use" {
|
||||||
st.InFuncBlock = true
|
st.InFuncBlock = true
|
||||||
st.CurrentFCID = cb.Get("id").String()
|
st.CurrentFCID = cb.Get("id").String()
|
||||||
name := cb.Get("name").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 := []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.Set(item, "sequence_number", nextSeq())
|
item, _ = sjson.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", idx)
|
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||||
item, _ = sjson.Set(item, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
||||||
item, _ = sjson.Set(item, "item.call_id", st.CurrentFCID)
|
item, _ = sjson.SetBytes(item, "item.call_id", st.CurrentFCID)
|
||||||
item, _ = sjson.Set(item, "item.name", name)
|
item, _ = sjson.SetBytes(item, "item.name", name)
|
||||||
out = append(out, emitEvent("response.output_item.added", item))
|
out = append(out, emitEvent("response.output_item.added", item))
|
||||||
if st.FuncArgsBuf[idx] == nil {
|
if st.FuncArgsBuf[idx] == nil {
|
||||||
st.FuncArgsBuf[idx] = &strings.Builder{}
|
st.FuncArgsBuf[idx] = &strings.Builder{}
|
||||||
@@ -160,16 +161,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
st.ReasoningIndex = idx
|
st.ReasoningIndex = idx
|
||||||
st.ReasoningBuf.Reset()
|
st.ReasoningBuf.Reset()
|
||||||
st.ReasoningItemID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx)
|
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 := []byte(`{"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.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", idx)
|
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||||
item, _ = sjson.Set(item, "item.id", st.ReasoningItemID)
|
item, _ = sjson.SetBytes(item, "item.id", st.ReasoningItemID)
|
||||||
out = append(out, emitEvent("response.output_item.added", item))
|
out = append(out, emitEvent("response.output_item.added", item))
|
||||||
// add a summary part placeholder
|
// 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 := []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.Set(part, "sequence_number", nextSeq())
|
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
|
||||||
part, _ = sjson.Set(part, "item_id", st.ReasoningItemID)
|
part, _ = sjson.SetBytes(part, "item_id", st.ReasoningItemID)
|
||||||
part, _ = sjson.Set(part, "output_index", idx)
|
part, _ = sjson.SetBytes(part, "output_index", idx)
|
||||||
out = append(out, emitEvent("response.reasoning_summary_part.added", part))
|
out = append(out, emitEvent("response.reasoning_summary_part.added", part))
|
||||||
st.ReasoningPartAdded = true
|
st.ReasoningPartAdded = true
|
||||||
}
|
}
|
||||||
@@ -181,10 +182,10 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
dt := d.Get("type").String()
|
dt := d.Get("type").String()
|
||||||
if dt == "text_delta" {
|
if dt == "text_delta" {
|
||||||
if t := d.Get("text"); t.Exists() {
|
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 := []byte(`{"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.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", st.CurrentMsgID)
|
msg, _ = sjson.SetBytes(msg, "item_id", st.CurrentMsgID)
|
||||||
msg, _ = sjson.Set(msg, "delta", t.String())
|
msg, _ = sjson.SetBytes(msg, "delta", t.String())
|
||||||
out = append(out, emitEvent("response.output_text.delta", msg))
|
out = append(out, emitEvent("response.output_text.delta", msg))
|
||||||
// aggregate text for response.output
|
// aggregate text for response.output
|
||||||
st.TextBuf.WriteString(t.String())
|
st.TextBuf.WriteString(t.String())
|
||||||
@@ -196,22 +197,22 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
st.FuncArgsBuf[idx] = &strings.Builder{}
|
st.FuncArgsBuf[idx] = &strings.Builder{}
|
||||||
}
|
}
|
||||||
st.FuncArgsBuf[idx].WriteString(pj.String())
|
st.FuncArgsBuf[idx].WriteString(pj.String())
|
||||||
msg := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`
|
msg := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`)
|
||||||
msg, _ = sjson.Set(msg, "sequence_number", nextSeq())
|
msg, _ = sjson.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
||||||
msg, _ = sjson.Set(msg, "output_index", idx)
|
msg, _ = sjson.SetBytes(msg, "output_index", idx)
|
||||||
msg, _ = sjson.Set(msg, "delta", pj.String())
|
msg, _ = sjson.SetBytes(msg, "delta", pj.String())
|
||||||
out = append(out, emitEvent("response.function_call_arguments.delta", msg))
|
out = append(out, emitEvent("response.function_call_arguments.delta", msg))
|
||||||
}
|
}
|
||||||
} else if dt == "thinking_delta" {
|
} else if dt == "thinking_delta" {
|
||||||
if st.ReasoningActive {
|
if st.ReasoningActive {
|
||||||
if t := d.Get("thinking"); t.Exists() {
|
if t := d.Get("thinking"); t.Exists() {
|
||||||
st.ReasoningBuf.WriteString(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 := []byte(`{"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.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", st.ReasoningItemID)
|
msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningItemID)
|
||||||
msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex)
|
msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex)
|
||||||
msg, _ = sjson.Set(msg, "delta", t.String())
|
msg, _ = sjson.SetBytes(msg, "delta", t.String())
|
||||||
out = append(out, emitEvent("response.reasoning_summary_text.delta", msg))
|
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":
|
case "content_block_stop":
|
||||||
idx := int(root.Get("index").Int())
|
idx := int(root.Get("index").Int())
|
||||||
if st.InTextBlock {
|
if st.InTextBlock {
|
||||||
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
done := []byte(`{"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.SetBytes(done, "sequence_number", nextSeq())
|
||||||
done, _ = sjson.Set(done, "item_id", st.CurrentMsgID)
|
done, _ = sjson.SetBytes(done, "item_id", st.CurrentMsgID)
|
||||||
out = append(out, emitEvent("response.output_text.done", done))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", st.CurrentMsgID)
|
partDone, _ = sjson.SetBytes(partDone, "item_id", st.CurrentMsgID)
|
||||||
out = append(out, emitEvent("response.content_part.done", partDone))
|
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 := []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.Set(final, "sequence_number", nextSeq())
|
final, _ = sjson.SetBytes(final, "sequence_number", nextSeq())
|
||||||
final, _ = sjson.Set(final, "item.id", st.CurrentMsgID)
|
final, _ = sjson.SetBytes(final, "item.id", st.CurrentMsgID)
|
||||||
out = append(out, emitEvent("response.output_item.done", final))
|
out = append(out, emitEvent("response.output_item.done", final))
|
||||||
st.InTextBlock = false
|
st.InTextBlock = false
|
||||||
} else if st.InFuncBlock {
|
} else if st.InFuncBlock {
|
||||||
@@ -239,34 +240,34 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
args = buf.String()
|
args = buf.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`
|
fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`)
|
||||||
fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq())
|
fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq())
|
||||||
fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
||||||
fcDone, _ = sjson.Set(fcDone, "output_index", idx)
|
fcDone, _ = sjson.SetBytes(fcDone, "output_index", idx)
|
||||||
fcDone, _ = sjson.Set(fcDone, "arguments", args)
|
fcDone, _ = sjson.SetBytes(fcDone, "arguments", args)
|
||||||
out = append(out, emitEvent("response.function_call_arguments.done", fcDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", idx)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", st.CurrentFCID))
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.arguments", args)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.call_id", st.CurrentFCID)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", st.CurrentFCID)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[idx])
|
itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[idx])
|
||||||
out = append(out, emitEvent("response.output_item.done", itemDone))
|
out = append(out, emitEvent("response.output_item.done", itemDone))
|
||||||
st.InFuncBlock = false
|
st.InFuncBlock = false
|
||||||
} else if st.ReasoningActive {
|
} else if st.ReasoningActive {
|
||||||
full := st.ReasoningBuf.String()
|
full := st.ReasoningBuf.String()
|
||||||
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
textDone := []byte(`{"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.SetBytes(textDone, "sequence_number", nextSeq())
|
||||||
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningItemID)
|
textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningItemID)
|
||||||
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex)
|
||||||
textDone, _ = sjson.Set(textDone, "text", full)
|
textDone, _ = sjson.SetBytes(textDone, "text", full)
|
||||||
out = append(out, emitEvent("response.reasoning_summary_text.done", textDone))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningItemID)
|
partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningItemID)
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex)
|
||||||
partDone, _ = sjson.Set(partDone, "part.text", full)
|
partDone, _ = sjson.SetBytes(partDone, "part.text", full)
|
||||||
out = append(out, emitEvent("response.reasoning_summary_part.done", partDone))
|
out = append(out, emitEvent("response.reasoning_summary_part.done", partDone))
|
||||||
st.ReasoningActive = false
|
st.ReasoningActive = false
|
||||||
st.ReasoningPartAdded = false
|
st.ReasoningPartAdded = false
|
||||||
@@ -284,92 +285,92 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
}
|
}
|
||||||
case "message_stop":
|
case "message_stop":
|
||||||
|
|
||||||
completed := `{"type":"response.completed","sequence_number":0,"response":{"id":"","object":"response","created_at":0,"status":"completed","background":false,"error":null}}`
|
completed := []byte(`{"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.SetBytes(completed, "sequence_number", nextSeq())
|
||||||
completed, _ = sjson.Set(completed, "response.id", st.ResponseID)
|
completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID)
|
||||||
completed, _ = sjson.Set(completed, "response.created_at", st.CreatedAt)
|
completed, _ = sjson.SetBytes(completed, "response.created_at", st.CreatedAt)
|
||||||
// Inject original request fields into response as per docs/response.completed.json
|
// Inject original request fields into response as per docs/response.completed.json
|
||||||
|
|
||||||
reqBytes := pickRequestJSON(originalRequestRawJSON, requestRawJSON)
|
reqBytes := pickRequestJSON(originalRequestRawJSON, requestRawJSON)
|
||||||
if len(reqBytes) > 0 {
|
if len(reqBytes) > 0 {
|
||||||
req := gjson.ParseBytes(reqBytes)
|
req := gjson.ParseBytes(reqBytes)
|
||||||
if v := req.Get("instructions"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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
|
// Build response.output from aggregated state
|
||||||
outputsWrapper := `{"arr":[]}`
|
outputsWrapper := []byte(`{"arr":[]}`)
|
||||||
// reasoning item (if any)
|
// reasoning item (if any)
|
||||||
if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
|
if st.ReasoningBuf.Len() > 0 || st.ReasoningPartAdded {
|
||||||
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`)
|
||||||
item, _ = sjson.Set(item, "id", st.ReasoningItemID)
|
item, _ = sjson.SetBytes(item, "id", st.ReasoningItemID)
|
||||||
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
|
item, _ = sjson.SetBytes(item, "summary.0.text", st.ReasoningBuf.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
// assistant message item (if any text)
|
// assistant message item (if any text)
|
||||||
if st.TextBuf.Len() > 0 || st.InTextBlock || st.CurrentMsgID != "" {
|
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 := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
|
||||||
item, _ = sjson.Set(item, "id", st.CurrentMsgID)
|
item, _ = sjson.SetBytes(item, "id", st.CurrentMsgID)
|
||||||
item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
|
item, _ = sjson.SetBytes(item, "content.0.text", st.TextBuf.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
// function_call items (in ascending index order for determinism)
|
// function_call items (in ascending index order for determinism)
|
||||||
if len(st.FuncArgsBuf) > 0 {
|
if len(st.FuncArgsBuf) > 0 {
|
||||||
@@ -396,16 +397,16 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
if callID == "" && st.CurrentFCID != "" {
|
if callID == "" && st.CurrentFCID != "" {
|
||||||
callID = st.CurrentFCID
|
callID = st.CurrentFCID
|
||||||
}
|
}
|
||||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
item, _ = sjson.Set(item, "arguments", args)
|
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||||
item, _ = sjson.Set(item, "call_id", callID)
|
item, _ = sjson.SetBytes(item, "call_id", callID)
|
||||||
item, _ = sjson.Set(item, "name", name)
|
item, _ = sjson.SetBytes(item, "name", name)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
reasoningTokens := int64(0)
|
reasoningTokens := int64(0)
|
||||||
@@ -414,15 +415,15 @@ func ConvertClaudeResponseToOpenAIResponses(ctx context.Context, modelName strin
|
|||||||
}
|
}
|
||||||
usagePresent := st.UsageSeen || reasoningTokens > 0
|
usagePresent := st.UsageSeen || reasoningTokens > 0
|
||||||
if usagePresent {
|
if usagePresent {
|
||||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.InputTokens)
|
completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", st.InputTokens)
|
||||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", 0)
|
completed, _ = sjson.SetBytes(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.output_tokens", st.OutputTokens)
|
||||||
if reasoningTokens > 0 {
|
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
|
total := st.InputTokens + st.OutputTokens
|
||||||
if total > 0 || st.UsageSeen {
|
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))
|
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.
|
// 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)
|
// Aggregate Claude SSE lines into a single OpenAI Responses JSON (non-stream)
|
||||||
// We follow the same aggregation logic as the streaming variant but produce
|
// We follow the same aggregation logic as the streaming variant but produce
|
||||||
// one final object matching docs/out.json structure.
|
// one final object matching docs/out.json structure.
|
||||||
@@ -455,7 +456,7 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Base OpenAI Responses (non-stream) object
|
// 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
|
// Aggregation state
|
||||||
var (
|
var (
|
||||||
@@ -557,88 +558,88 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Populate base fields
|
// Populate base fields
|
||||||
out, _ = sjson.Set(out, "id", responseID)
|
out, _ = sjson.SetBytes(out, "id", responseID)
|
||||||
out, _ = sjson.Set(out, "created_at", createdAt)
|
out, _ = sjson.SetBytes(out, "created_at", createdAt)
|
||||||
|
|
||||||
// Inject request echo fields as top-level (similar to streaming variant)
|
// Inject request echo fields as top-level (similar to streaming variant)
|
||||||
reqBytes := pickRequestJSON(originalRequestRawJSON, requestRawJSON)
|
reqBytes := pickRequestJSON(originalRequestRawJSON, requestRawJSON)
|
||||||
if len(reqBytes) > 0 {
|
if len(reqBytes) > 0 {
|
||||||
req := gjson.ParseBytes(reqBytes)
|
req := gjson.ParseBytes(reqBytes)
|
||||||
if v := req.Get("instructions"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
if v := req.Get("metadata"); v.Exists() {
|
||||||
out, _ = sjson.Set(out, "metadata", v.Value())
|
out, _ = sjson.SetBytes(out, "metadata", v.Value())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build output array
|
// Build output array
|
||||||
outputsWrapper := `{"arr":[]}`
|
outputsWrapper := []byte(`{"arr":[]}`)
|
||||||
if reasoningBuf.Len() > 0 {
|
if reasoningBuf.Len() > 0 {
|
||||||
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`)
|
||||||
item, _ = sjson.Set(item, "id", reasoningItemID)
|
item, _ = sjson.SetBytes(item, "id", reasoningItemID)
|
||||||
item, _ = sjson.Set(item, "summary.0.text", reasoningBuf.String())
|
item, _ = sjson.SetBytes(item, "summary.0.text", reasoningBuf.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
if currentMsgID != "" || textBuf.Len() > 0 {
|
if currentMsgID != "" || textBuf.Len() > 0 {
|
||||||
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
|
||||||
item, _ = sjson.Set(item, "id", currentMsgID)
|
item, _ = sjson.SetBytes(item, "id", currentMsgID)
|
||||||
item, _ = sjson.Set(item, "content.0.text", textBuf.String())
|
item, _ = sjson.SetBytes(item, "content.0.text", textBuf.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
if len(toolCalls) > 0 {
|
if len(toolCalls) > 0 {
|
||||||
// Preserve index order
|
// Preserve index order
|
||||||
@@ -659,28 +660,28 @@ func ConvertClaudeResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
if args == "" {
|
if args == "" {
|
||||||
args = "{}"
|
args = "{}"
|
||||||
}
|
}
|
||||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", st.id))
|
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", st.id))
|
||||||
item, _ = sjson.Set(item, "arguments", args)
|
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||||
item, _ = sjson.Set(item, "call_id", st.id)
|
item, _ = sjson.SetBytes(item, "call_id", st.id)
|
||||||
item, _ = sjson.Set(item, "name", st.name)
|
item, _ = sjson.SetBytes(item, "name", st.name)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "output", gjson.Get(outputsWrapper, "arr").Raw)
|
out, _ = sjson.SetRawBytes(out, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usage
|
// Usage
|
||||||
total := inputTokens + outputTokens
|
total := inputTokens + outputTokens
|
||||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.total_tokens", total)
|
out, _ = sjson.SetBytes(out, "usage.total_tokens", total)
|
||||||
if reasoningBuf.Len() > 0 {
|
if reasoningBuf.Len() > 0 {
|
||||||
// Rough estimate similar to chat completions
|
// Rough estimate similar to chat completions
|
||||||
reasoningTokens := int64(len(reasoningBuf.String()) / 4)
|
reasoningTokens := int64(len(reasoningBuf.String()) / 4)
|
||||||
if reasoningTokens > 0 {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,15 +36,15 @@ import (
|
|||||||
func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
template := `{"model":"","instructions":"","input":[]}`
|
template := []byte(`{"model":"","instructions":"","input":[]}`)
|
||||||
|
|
||||||
rootResult := gjson.ParseBytes(rawJSON)
|
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.
|
// Process system messages and convert them to input content format.
|
||||||
systemsResult := rootResult.Get("system")
|
systemsResult := rootResult.Get("system")
|
||||||
if systemsResult.Exists() {
|
if systemsResult.Exists() {
|
||||||
message := `{"type":"message","role":"developer","content":[]}`
|
message := []byte(`{"type":"message","role":"developer","content":[]}`)
|
||||||
contentIndex := 0
|
contentIndex := 0
|
||||||
|
|
||||||
appendSystemText := func(text string) {
|
appendSystemText := func(text string) {
|
||||||
@@ -52,8 +52,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_text")
|
message, _ = sjson.SetBytes(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.text", contentIndex), text)
|
||||||
contentIndex++
|
contentIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if contentIndex > 0 {
|
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]
|
messageResult := messageResults[i]
|
||||||
messageRole := messageResult.Get("role").String()
|
messageRole := messageResult.Get("role").String()
|
||||||
|
|
||||||
newMessage := func() string {
|
newMessage := func() []byte {
|
||||||
msg := `{"type": "message","role":"","content":[]}`
|
msg := []byte(`{"type":"message","role":"","content":[]}`)
|
||||||
msg, _ = sjson.Set(msg, "role", messageRole)
|
msg, _ = sjson.SetBytes(msg, "role", messageRole)
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
|
|
||||||
flushMessage := func() {
|
flushMessage := func() {
|
||||||
if hasContent {
|
if hasContent {
|
||||||
template, _ = sjson.SetRaw(template, "input.-1", message)
|
template, _ = sjson.SetRawBytes(template, "input.-1", message)
|
||||||
message = newMessage()
|
message = newMessage()
|
||||||
contentIndex = 0
|
contentIndex = 0
|
||||||
hasContent = false
|
hasContent = false
|
||||||
@@ -107,15 +107,15 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if messageRole == "assistant" {
|
if messageRole == "assistant" {
|
||||||
partType = "output_text"
|
partType = "output_text"
|
||||||
}
|
}
|
||||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), partType)
|
message, _ = sjson.SetBytes(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.text", contentIndex), text)
|
||||||
contentIndex++
|
contentIndex++
|
||||||
hasContent = true
|
hasContent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
appendImageContent := func(dataURL string) {
|
appendImageContent := func(dataURL string) {
|
||||||
message, _ = sjson.Set(message, fmt.Sprintf("content.%d.type", contentIndex), "input_image")
|
message, _ = sjson.SetBytes(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.image_url", contentIndex), dataURL)
|
||||||
contentIndex++
|
contentIndex++
|
||||||
hasContent = true
|
hasContent = true
|
||||||
}
|
}
|
||||||
@@ -151,8 +151,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
flushMessage()
|
flushMessage()
|
||||||
functionCallMessage := `{"type":"function_call"}`
|
functionCallMessage := []byte(`{"type":"function_call"}`)
|
||||||
functionCallMessage, _ = sjson.Set(functionCallMessage, "call_id", messageContentResult.Get("id").String())
|
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "call_id", messageContentResult.Get("id").String())
|
||||||
{
|
{
|
||||||
name := messageContentResult.Get("name").String()
|
name := messageContentResult.Get("name").String()
|
||||||
toolMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
|
toolMap := buildReverseMapFromClaudeOriginalToShort(rawJSON)
|
||||||
@@ -161,19 +161,19 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
} else {
|
} else {
|
||||||
name = shortenNameIfNeeded(name)
|
name = shortenNameIfNeeded(name)
|
||||||
}
|
}
|
||||||
functionCallMessage, _ = sjson.Set(functionCallMessage, "name", name)
|
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "name", name)
|
||||||
}
|
}
|
||||||
functionCallMessage, _ = sjson.Set(functionCallMessage, "arguments", messageContentResult.Get("input").Raw)
|
functionCallMessage, _ = sjson.SetBytes(functionCallMessage, "arguments", messageContentResult.Get("input").Raw)
|
||||||
template, _ = sjson.SetRaw(template, "input.-1", functionCallMessage)
|
template, _ = sjson.SetRawBytes(template, "input.-1", functionCallMessage)
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
flushMessage()
|
flushMessage()
|
||||||
functionCallOutputMessage := `{"type":"function_call_output"}`
|
functionCallOutputMessage := []byte(`{"type":"function_call_output"}`)
|
||||||
functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String())
|
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "call_id", messageContentResult.Get("tool_use_id").String())
|
||||||
|
|
||||||
contentResult := messageContentResult.Get("content")
|
contentResult := messageContentResult.Get("content")
|
||||||
if contentResult.IsArray() {
|
if contentResult.IsArray() {
|
||||||
toolResultContentIndex := 0
|
toolResultContentIndex := 0
|
||||||
toolResultContent := `[]`
|
toolResultContent := []byte(`[]`)
|
||||||
contentResults := contentResult.Array()
|
contentResults := contentResult.Array()
|
||||||
for k := 0; k < len(contentResults); k++ {
|
for k := 0; k < len(contentResults); k++ {
|
||||||
toolResultContentType := contentResults[k].Get("type").String()
|
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)
|
dataURL := fmt.Sprintf("data:%s;base64,%s", mediaType, data)
|
||||||
|
|
||||||
toolResultContent, _ = sjson.Set(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_image")
|
toolResultContent, _ = sjson.SetBytes(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.image_url", toolResultContentIndex), dataURL)
|
||||||
toolResultContentIndex++
|
toolResultContentIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if toolResultContentType == "text" {
|
} else if toolResultContentType == "text" {
|
||||||
toolResultContent, _ = sjson.Set(toolResultContent, fmt.Sprintf("%d.type", toolResultContentIndex), "input_text")
|
toolResultContent, _ = sjson.SetBytes(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.text", toolResultContentIndex), contentResults[k].Get("text").String())
|
||||||
toolResultContentIndex++
|
toolResultContentIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if toolResultContent != `[]` {
|
if toolResultContentIndex > 0 {
|
||||||
functionCallOutputMessage, _ = sjson.SetRaw(functionCallOutputMessage, "output", toolResultContent)
|
functionCallOutputMessage, _ = sjson.SetRawBytes(functionCallOutputMessage, "output", toolResultContent)
|
||||||
} else {
|
} else {
|
||||||
functionCallOutputMessage, _ = sjson.Set(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
|
functionCallOutputMessage, _ = sjson.SetBytes(functionCallOutputMessage, "output", messageContentResult.Get("content").String())
|
||||||
}
|
}
|
||||||
} else {
|
} 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()
|
flushMessage()
|
||||||
@@ -229,8 +229,8 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// Convert tools declarations to the expected format for the Codex API.
|
// Convert tools declarations to the expected format for the Codex API.
|
||||||
toolsResult := rootResult.Get("tools")
|
toolsResult := rootResult.Get("tools")
|
||||||
if toolsResult.IsArray() {
|
if toolsResult.IsArray() {
|
||||||
template, _ = sjson.SetRaw(template, "tools", `[]`)
|
template, _ = sjson.SetRawBytes(template, "tools", []byte(`[]`))
|
||||||
template, _ = sjson.Set(template, "tool_choice", `auto`)
|
template, _ = sjson.SetBytes(template, "tool_choice", `auto`)
|
||||||
toolResults := toolsResult.Array()
|
toolResults := toolsResult.Array()
|
||||||
// Build short name map from declared tools
|
// Build short name map from declared tools
|
||||||
var names []string
|
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
|
// Special handling: map Claude web search tool to Codex web_search
|
||||||
if toolResult.Get("type").String() == "web_search_20250305" {
|
if toolResult.Get("type").String() == "web_search_20250305" {
|
||||||
// Replace the tool content entirely with {"type":"web_search"}
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
tool := toolResult.Raw
|
tool := []byte(toolResult.Raw)
|
||||||
tool, _ = sjson.Set(tool, "type", "function")
|
tool, _ = sjson.SetBytes(tool, "type", "function")
|
||||||
// Apply shortened name if needed
|
// Apply shortened name if needed
|
||||||
if v := toolResult.Get("name"); v.Exists() {
|
if v := toolResult.Get("name"); v.Exists() {
|
||||||
name := v.String()
|
name := v.String()
|
||||||
@@ -259,20 +259,26 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
} else {
|
} else {
|
||||||
name = shortenNameIfNeeded(name)
|
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.SetRawBytes(tool, "parameters", []byte(normalizeToolParameters(toolResult.Get("input_schema").Raw)))
|
||||||
tool, _ = sjson.Delete(tool, "input_schema")
|
tool, _ = sjson.DeleteBytes(tool, "input_schema")
|
||||||
tool, _ = sjson.Delete(tool, "parameters.$schema")
|
tool, _ = sjson.DeleteBytes(tool, "parameters.$schema")
|
||||||
tool, _ = sjson.Delete(tool, "cache_control")
|
tool, _ = sjson.DeleteBytes(tool, "cache_control")
|
||||||
tool, _ = sjson.Delete(tool, "defer_loading")
|
tool, _ = sjson.DeleteBytes(tool, "defer_loading")
|
||||||
tool, _ = sjson.Set(tool, "strict", false)
|
tool, _ = sjson.SetBytes(tool, "strict", false)
|
||||||
template, _ = sjson.SetRaw(template, "tools.-1", tool)
|
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.
|
// 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.
|
// Convert thinking.budget_tokens to reasoning.effort.
|
||||||
reasoningEffort := "medium"
|
reasoningEffort := "medium"
|
||||||
@@ -303,13 +309,13 @@ func ConvertClaudeRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "reasoning.effort", reasoningEffort)
|
template, _ = sjson.SetBytes(template, "reasoning.effort", reasoningEffort)
|
||||||
template, _ = sjson.Set(template, "reasoning.summary", "auto")
|
template, _ = sjson.SetBytes(template, "reasoning.summary", "auto")
|
||||||
template, _ = sjson.Set(template, "stream", true)
|
template, _ = sjson.SetBytes(template, "stream", true)
|
||||||
template, _ = sjson.Set(template, "store", false)
|
template, _ = sjson.SetBytes(template, "store", false)
|
||||||
template, _ = sjson.Set(template, "include", []string{"reasoning.encrypted_content"})
|
template, _ = sjson.SetBytes(template, "include", []string{"reasoning.encrypted_content"})
|
||||||
|
|
||||||
return []byte(template)
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortenNameIfNeeded applies a simple shortening rule for a single name.
|
// 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) {
|
if raw == "" || raw == "null" || !gjson.Valid(raw) {
|
||||||
return `{"type":"object","properties":{}}`
|
return `{"type":"object","properties":{}}`
|
||||||
}
|
}
|
||||||
schema := raw
|
|
||||||
result := gjson.Parse(raw)
|
result := gjson.Parse(raw)
|
||||||
|
schema := []byte(raw)
|
||||||
schemaType := result.Get("type").String()
|
schemaType := result.Get("type").String()
|
||||||
if schemaType == "" {
|
if schemaType == "" {
|
||||||
schema, _ = sjson.Set(schema, "type", "object")
|
schema, _ = sjson.SetBytes(schema, "type", "object")
|
||||||
schemaType = "object"
|
schemaType = "object"
|
||||||
}
|
}
|
||||||
if schemaType == "object" && !result.Get("properties").Exists() {
|
if schemaType == "object" && !result.Get("properties").Exists() {
|
||||||
schema, _ = sjson.SetRaw(schema, "properties", `{}`)
|
schema, _ = sjson.SetRawBytes(schema, "properties", []byte(`{}`))
|
||||||
}
|
}
|
||||||
return schema
|
return string(schema)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ package claude
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -43,8 +43,8 @@ type ConvertCodexResponseToClaudeParams struct {
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Claude Code-compatible JSON response
|
// - [][]byte: A slice of Claude Code-compatible JSON responses
|
||||||
func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &ConvertCodexResponseToClaudeParams{
|
*param = &ConvertCodexResponseToClaudeParams{
|
||||||
HasToolCall: false,
|
HasToolCall: false,
|
||||||
@@ -54,95 +54,85 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
|
|
||||||
// log.Debugf("rawJSON: %s", string(rawJSON))
|
// log.Debugf("rawJSON: %s", string(rawJSON))
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
output := ""
|
output := make([]byte, 0, 512)
|
||||||
rootResult := gjson.ParseBytes(rawJSON)
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
typeResult := rootResult.Get("type")
|
typeResult := rootResult.Get("type")
|
||||||
typeStr := typeResult.String()
|
typeStr := typeResult.String()
|
||||||
template := ""
|
var template []byte
|
||||||
if typeStr == "response.created" {
|
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 = []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.Set(template, "message.model", rootResult.Get("response.model").String())
|
template, _ = sjson.SetBytes(template, "message.model", rootResult.Get("response.model").String())
|
||||||
template, _ = sjson.Set(template, "message.id", rootResult.Get("response.id").String())
|
template, _ = sjson.SetBytes(template, "message.id", rootResult.Get("response.id").String())
|
||||||
|
|
||||||
output = "event: message_start\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "message_start", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.reasoning_summary_part.added" {
|
} else if typeStr == "response.reasoning_summary_part.added" {
|
||||||
template = `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
|
|
||||||
output = "event: content_block_start\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.reasoning_summary_text.delta" {
|
} else if typeStr == "response.reasoning_summary_text.delta" {
|
||||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
template, _ = sjson.Set(template, "delta.thinking", rootResult.Get("delta").String())
|
template, _ = sjson.SetBytes(template, "delta.thinking", rootResult.Get("delta").String())
|
||||||
|
|
||||||
output = "event: content_block_delta\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.reasoning_summary_part.done" {
|
} else if typeStr == "response.reasoning_summary_part.done" {
|
||||||
template = `{"type":"content_block_stop","index":0}`
|
template = []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
(*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++
|
(*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++
|
||||||
|
|
||||||
output = "event: content_block_stop\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
|
|
||||||
} else if typeStr == "response.content_part.added" {
|
} else if typeStr == "response.content_part.added" {
|
||||||
template = `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
|
|
||||||
output = "event: content_block_start\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.output_text.delta" {
|
} else if typeStr == "response.output_text.delta" {
|
||||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
template, _ = sjson.Set(template, "delta.text", rootResult.Get("delta").String())
|
template, _ = sjson.SetBytes(template, "delta.text", rootResult.Get("delta").String())
|
||||||
|
|
||||||
output = "event: content_block_delta\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.content_part.done" {
|
} else if typeStr == "response.content_part.done" {
|
||||||
template = `{"type":"content_block_stop","index":0}`
|
template = []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
(*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++
|
(*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++
|
||||||
|
|
||||||
output = "event: content_block_stop\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.completed" {
|
} 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
|
p := (*param).(*ConvertCodexResponseToClaudeParams).HasToolCall
|
||||||
stopReason := rootResult.Get("response.stop_reason").String()
|
stopReason := rootResult.Get("response.stop_reason").String()
|
||||||
if p {
|
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" {
|
} else if stopReason == "max_tokens" || stopReason == "stop" {
|
||||||
template, _ = sjson.Set(template, "delta.stop_reason", stopReason)
|
template, _ = sjson.SetBytes(template, "delta.stop_reason", stopReason)
|
||||||
} else {
|
} 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"))
|
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(rootResult.Get("response.usage"))
|
||||||
template, _ = sjson.Set(template, "usage.input_tokens", inputTokens)
|
template, _ = sjson.SetBytes(template, "usage.input_tokens", inputTokens)
|
||||||
template, _ = sjson.Set(template, "usage.output_tokens", outputTokens)
|
template, _ = sjson.SetBytes(template, "usage.output_tokens", outputTokens)
|
||||||
if cachedTokens > 0 {
|
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 = translatorcommon.AppendSSEEventBytes(output, "message_delta", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
output = translatorcommon.AppendSSEEventBytes(output, "message_stop", []byte(`{"type":"message_stop"}`), 2)
|
||||||
output += "event: message_stop\n"
|
|
||||||
output += `data: {"type":"message_stop"}`
|
|
||||||
output += "\n\n"
|
|
||||||
} else if typeStr == "response.output_item.added" {
|
} else if typeStr == "response.output_item.added" {
|
||||||
itemResult := rootResult.Get("item")
|
itemResult := rootResult.Get("item")
|
||||||
itemType := itemResult.Get("type").String()
|
itemType := itemResult.Get("type").String()
|
||||||
if itemType == "function_call" {
|
if itemType == "function_call" {
|
||||||
(*param).(*ConvertCodexResponseToClaudeParams).HasToolCall = true
|
(*param).(*ConvertCodexResponseToClaudeParams).HasToolCall = true
|
||||||
(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = false
|
(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = false
|
||||||
template = `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
template = []byte(`{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
template, _ = sjson.Set(template, "content_block.id", util.SanitizeClaudeToolID(itemResult.Get("call_id").String()))
|
template, _ = sjson.SetBytes(template, "content_block.id", util.SanitizeClaudeToolID(itemResult.Get("call_id").String()))
|
||||||
{
|
{
|
||||||
// Restore original tool name if shortened
|
// Restore original tool name if shortened
|
||||||
name := itemResult.Get("name").String()
|
name := itemResult.Get("name").String()
|
||||||
@@ -150,37 +140,33 @@ func ConvertCodexResponseToClaude(_ context.Context, _ string, originalRequestRa
|
|||||||
if orig, ok := rev[name]; ok {
|
if orig, ok := rev[name]; ok {
|
||||||
name = orig
|
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 = translatorcommon.AppendSSEEventBytes(output, "content_block_start", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
|
|
||||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
|
|
||||||
output += "event: content_block_delta\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
}
|
}
|
||||||
} else if typeStr == "response.output_item.done" {
|
} else if typeStr == "response.output_item.done" {
|
||||||
itemResult := rootResult.Get("item")
|
itemResult := rootResult.Get("item")
|
||||||
itemType := itemResult.Get("type").String()
|
itemType := itemResult.Get("type").String()
|
||||||
if itemType == "function_call" {
|
if itemType == "function_call" {
|
||||||
template = `{"type":"content_block_stop","index":0}`
|
template = []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
(*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++
|
(*param).(*ConvertCodexResponseToClaudeParams).BlockIndex++
|
||||||
|
|
||||||
output = "event: content_block_stop\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_stop", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
}
|
}
|
||||||
} else if typeStr == "response.function_call_arguments.delta" {
|
} else if typeStr == "response.function_call_arguments.delta" {
|
||||||
(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = true
|
(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta = true
|
||||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
template, _ = sjson.Set(template, "delta.partial_json", rootResult.Get("delta").String())
|
template, _ = sjson.SetBytes(template, "delta.partial_json", rootResult.Get("delta").String())
|
||||||
|
|
||||||
output += "event: content_block_delta\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
} else if typeStr == "response.function_call_arguments.done" {
|
} else if typeStr == "response.function_call_arguments.done" {
|
||||||
// Some models (e.g. gpt-5.3-codex-spark) send function call arguments
|
// Some models (e.g. gpt-5.3-codex-spark) send function call arguments
|
||||||
// in a single "done" event without preceding "delta" events.
|
// 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.
|
// When delta events were already received, skip to avoid duplicating arguments.
|
||||||
if !(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta {
|
if !(*param).(*ConvertCodexResponseToClaudeParams).HasReceivedArgumentsDelta {
|
||||||
if args := rootResult.Get("arguments").String(); args != "" {
|
if args := rootResult.Get("arguments").String(); args != "" {
|
||||||
template = `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
template = []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||||
template, _ = sjson.Set(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
template, _ = sjson.SetBytes(template, "index", (*param).(*ConvertCodexResponseToClaudeParams).BlockIndex)
|
||||||
template, _ = sjson.Set(template, "delta.partial_json", args)
|
template, _ = sjson.SetBytes(template, "delta.partial_json", args)
|
||||||
|
|
||||||
output += "event: content_block_delta\n"
|
output = translatorcommon.AppendSSEEventBytes(output, "content_block_delta", template, 2)
|
||||||
output += fmt.Sprintf("data: %s\n\n", template)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{output}
|
return [][]byte{output}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCodexResponseToClaudeNonStream converts a non-streaming Codex response to a non-streaming Claude Code response.
|
// 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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Claude Code-compatible JSON response containing all message content and metadata
|
// - []byte: A Claude Code-compatible JSON response containing all message content and metadata
|
||||||
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) string {
|
func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, _ []byte, rawJSON []byte, _ *any) []byte {
|
||||||
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
revNames := buildReverseMapFromClaudeOriginalShortToOriginal(originalRequestRawJSON)
|
||||||
|
|
||||||
rootResult := gjson.ParseBytes(rawJSON)
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
if rootResult.Get("type").String() != "response.completed" {
|
if rootResult.Get("type").String() != "response.completed" {
|
||||||
return ""
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData := rootResult.Get("response")
|
responseData := rootResult.Get("response")
|
||||||
if !responseData.Exists() {
|
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 := []byte(`{"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.SetBytes(out, "id", responseData.Get("id").String())
|
||||||
out, _ = sjson.Set(out, "model", responseData.Get("model").String())
|
out, _ = sjson.SetBytes(out, "model", responseData.Get("model").String())
|
||||||
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage"))
|
inputTokens, outputTokens, cachedTokens := extractResponsesUsage(responseData.Get("usage"))
|
||||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||||
if cachedTokens > 0 {
|
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
|
hasToolCall := false
|
||||||
@@ -276,9 +261,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if thinkingBuilder.Len() > 0 {
|
if thinkingBuilder.Len() > 0 {
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
case "message":
|
case "message":
|
||||||
if content := item.Get("content"); content.Exists() {
|
if content := item.Get("content"); content.Exists() {
|
||||||
@@ -287,9 +272,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
if part.Get("type").String() == "output_text" {
|
if part.Get("type").String() == "output_text" {
|
||||||
text := part.Get("text").String()
|
text := part.Get("text").String()
|
||||||
if text != "" {
|
if text != "" {
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", text)
|
block, _ = sjson.SetBytes(block, "text", text)
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -297,9 +282,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
} else {
|
} else {
|
||||||
text := content.String()
|
text := content.String()
|
||||||
if text != "" {
|
if text != "" {
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", text)
|
block, _ = sjson.SetBytes(block, "text", text)
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,9 +295,9 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
name = original
|
name = original
|
||||||
}
|
}
|
||||||
|
|
||||||
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "id", util.SanitizeClaudeToolID(item.Get("call_id").String()))
|
toolBlock, _ = sjson.SetBytes(toolBlock, "id", util.SanitizeClaudeToolID(item.Get("call_id").String()))
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
toolBlock, _ = sjson.SetBytes(toolBlock, "name", name)
|
||||||
inputRaw := "{}"
|
inputRaw := "{}"
|
||||||
if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr := item.Get("arguments").String(); argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsJSON := gjson.Parse(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
@@ -320,23 +305,23 @@ func ConvertCodexResponseToClaudeNonStream(_ context.Context, _ string, original
|
|||||||
inputRaw = argsJSON.Raw
|
inputRaw = argsJSON.Raw
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
|
toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw))
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
|
out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if stopReason := responseData.Get("stop_reason"); stopReason.Exists() && stopReason.String() != "" {
|
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 {
|
} else if hasToolCall {
|
||||||
out, _ = sjson.Set(out, "stop_reason", "tool_use")
|
out, _ = sjson.SetBytes(out, "stop_reason", "tool_use")
|
||||||
} else {
|
} 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() != "" {
|
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
|
return out
|
||||||
@@ -386,6 +371,6 @@ func buildReverseMapFromClaudeOriginalShortToOriginal(original []byte) map[strin
|
|||||||
return rev
|
return rev
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ package geminiCLI
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
. "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/codex/gemini"
|
. "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.
|
// 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
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response wrapped in a response object
|
// - [][]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) []string {
|
func ConvertCodexResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
outputs := ConvertCodexResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
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++ {
|
for i := 0; i < len(outputs); i++ {
|
||||||
json := `{"response": {}}`
|
newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i]))
|
||||||
output, _ := sjson.SetRaw(json, "response", outputs[i])
|
|
||||||
newOutputs = append(newOutputs, output)
|
|
||||||
}
|
}
|
||||||
return newOutputs
|
return newOutputs
|
||||||
}
|
}
|
||||||
@@ -47,15 +44,12 @@ func ConvertCodexResponseToGeminiCLI(ctx context.Context, modelName string, orig
|
|||||||
// - param: A pointer to a parameter object for the conversion
|
// - param: A pointer to a parameter object for the conversion
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response wrapped in a response object
|
// - []byte: A Gemini-compatible JSON response wrapped in a response object
|
||||||
func ConvertCodexResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
func ConvertCodexResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||||
// log.Debug(string(rawJSON))
|
out := ConvertCodexResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
strJSON := ConvertCodexResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
return translatorcommon.WrapGeminiCLIResponse(out)
|
||||||
json := `{"response": {}}`
|
|
||||||
strJSON, _ = sjson.SetRaw(json, "response", strJSON)
|
|
||||||
return strJSON
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiCLITokenCount(ctx context.Context, count int64) string {
|
func GeminiCLITokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import (
|
|||||||
func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
// Base template
|
// Base template
|
||||||
out := `{"model":"","instructions":"","input":[]}`
|
out := []byte(`{"model":"","instructions":"","input":[]}`)
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
@@ -82,24 +82,24 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model
|
// Model
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// System instruction -> as a user message with input_text parts
|
// System instruction -> as a user message with input_text parts
|
||||||
sysParts := root.Get("system_instruction.parts")
|
sysParts := root.Get("system_instruction.parts")
|
||||||
if sysParts.IsArray() {
|
if sysParts.IsArray() {
|
||||||
msg := `{"type":"message","role":"developer","content":[]}`
|
msg := []byte(`{"type":"message","role":"developer","content":[]}`)
|
||||||
arr := sysParts.Array()
|
arr := sysParts.Array()
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
p := arr[i]
|
p := arr[i]
|
||||||
if t := p.Get("text"); t.Exists() {
|
if t := p.Get("text"); t.Exists() {
|
||||||
part := `{}`
|
part := []byte(`{}`)
|
||||||
part, _ = sjson.Set(part, "type", "input_text")
|
part, _ = sjson.SetBytes(part, "type", "input_text")
|
||||||
part, _ = sjson.Set(part, "text", t.String())
|
part, _ = sjson.SetBytes(part, "text", t.String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(gjson.Get(msg, "content").Array()) > 0 {
|
if len(gjson.GetBytes(msg, "content").Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "input.-1", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,23 +123,23 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
p := parr[j]
|
p := parr[j]
|
||||||
// text part
|
// text part
|
||||||
if t := p.Get("text"); t.Exists() {
|
if t := p.Get("text"); t.Exists() {
|
||||||
msg := `{"type":"message","role":"","content":[]}`
|
msg := []byte(`{"type":"message","role":"","content":[]}`)
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||||
partType := "input_text"
|
partType := "input_text"
|
||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
partType = "output_text"
|
partType = "output_text"
|
||||||
}
|
}
|
||||||
part := `{}`
|
part := []byte(`{}`)
|
||||||
part, _ = sjson.Set(part, "type", partType)
|
part, _ = sjson.SetBytes(part, "type", partType)
|
||||||
part, _ = sjson.Set(part, "text", t.String())
|
part, _ = sjson.SetBytes(part, "text", t.String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", part)
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "input.-1", msg)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// function call from model
|
// function call from model
|
||||||
if fc := p.Get("functionCall"); fc.Exists() {
|
if fc := p.Get("functionCall"); fc.Exists() {
|
||||||
fn := `{"type":"function_call"}`
|
fn := []byte(`{"type":"function_call"}`)
|
||||||
if name := fc.Get("name"); name.Exists() {
|
if name := fc.Get("name"); name.Exists() {
|
||||||
n := name.String()
|
n := name.String()
|
||||||
if short, ok := shortMap[n]; ok {
|
if short, ok := shortMap[n]; ok {
|
||||||
@@ -147,31 +147,31 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
} else {
|
} else {
|
||||||
n = shortenNameIfNeeded(n)
|
n = shortenNameIfNeeded(n)
|
||||||
}
|
}
|
||||||
fn, _ = sjson.Set(fn, "name", n)
|
fn, _ = sjson.SetBytes(fn, "name", n)
|
||||||
}
|
}
|
||||||
if args := fc.Get("args"); args.Exists() {
|
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
|
// generate a paired random call_id and enqueue it so the
|
||||||
// corresponding functionResponse can pop the earliest id
|
// corresponding functionResponse can pop the earliest id
|
||||||
// to preserve ordering when multiple calls are present.
|
// to preserve ordering when multiple calls are present.
|
||||||
id := genCallID()
|
id := genCallID()
|
||||||
fn, _ = sjson.Set(fn, "call_id", id)
|
fn, _ = sjson.SetBytes(fn, "call_id", id)
|
||||||
pendingCallIDs = append(pendingCallIDs, id)
|
pendingCallIDs = append(pendingCallIDs, id)
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", fn)
|
out, _ = sjson.SetRawBytes(out, "input.-1", fn)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// function response from user
|
// function response from user
|
||||||
if fr := p.Get("functionResponse"); fr.Exists() {
|
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
|
// Prefer a string result if present; otherwise embed the raw response as a string
|
||||||
if res := fr.Get("response.result"); res.Exists() {
|
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() {
|
} 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
|
// attach the oldest queued call_id to pair the response
|
||||||
// with its call. If the queue is empty, generate a new id.
|
// with its call. If the queue is empty, generate a new id.
|
||||||
var id string
|
var id string
|
||||||
@@ -182,8 +182,8 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
} else {
|
} else {
|
||||||
id = genCallID()
|
id = genCallID()
|
||||||
}
|
}
|
||||||
fno, _ = sjson.Set(fno, "call_id", id)
|
fno, _ = sjson.SetBytes(fno, "call_id", id)
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", fno)
|
out, _ = sjson.SetRawBytes(out, "input.-1", fno)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,8 +193,8 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
// Tools mapping: Gemini functionDeclarations -> Codex tools
|
// Tools mapping: Gemini functionDeclarations -> Codex tools
|
||||||
tools := root.Get("tools")
|
tools := root.Get("tools")
|
||||||
if tools.IsArray() {
|
if tools.IsArray() {
|
||||||
out, _ = sjson.SetRaw(out, "tools", `[]`)
|
out, _ = sjson.SetRawBytes(out, "tools", []byte(`[]`))
|
||||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
|
||||||
tarr := tools.Array()
|
tarr := tools.Array()
|
||||||
for i := 0; i < len(tarr); i++ {
|
for i := 0; i < len(tarr); i++ {
|
||||||
td := tarr[i]
|
td := tarr[i]
|
||||||
@@ -205,8 +205,8 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
farr := fns.Array()
|
farr := fns.Array()
|
||||||
for j := 0; j < len(farr); j++ {
|
for j := 0; j < len(farr); j++ {
|
||||||
fn := farr[j]
|
fn := farr[j]
|
||||||
tool := `{}`
|
tool := []byte(`{}`)
|
||||||
tool, _ = sjson.Set(tool, "type", "function")
|
tool, _ = sjson.SetBytes(tool, "type", "function")
|
||||||
if v := fn.Get("name"); v.Exists() {
|
if v := fn.Get("name"); v.Exists() {
|
||||||
name := v.String()
|
name := v.String()
|
||||||
if short, ok := shortMap[name]; ok {
|
if short, ok := shortMap[name]; ok {
|
||||||
@@ -214,32 +214,32 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
} else {
|
} else {
|
||||||
name = shortenNameIfNeeded(name)
|
name = shortenNameIfNeeded(name)
|
||||||
}
|
}
|
||||||
tool, _ = sjson.Set(tool, "name", name)
|
tool, _ = sjson.SetBytes(tool, "name", name)
|
||||||
}
|
}
|
||||||
if v := fn.Get("description"); v.Exists() {
|
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() {
|
if prm := fn.Get("parameters"); prm.Exists() {
|
||||||
// Remove optional $schema field if present
|
// Remove optional $schema field if present
|
||||||
cleaned := prm.Raw
|
cleaned := []byte(prm.Raw)
|
||||||
cleaned, _ = sjson.Delete(cleaned, "$schema")
|
cleaned, _ = sjson.DeleteBytes(cleaned, "$schema")
|
||||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false)
|
||||||
tool, _ = sjson.SetRaw(tool, "parameters", cleaned)
|
tool, _ = sjson.SetRawBytes(tool, "parameters", cleaned)
|
||||||
} else if prm = fn.Get("parametersJsonSchema"); prm.Exists() {
|
} else if prm = fn.Get("parametersJsonSchema"); prm.Exists() {
|
||||||
// Remove optional $schema field if present
|
// Remove optional $schema field if present
|
||||||
cleaned := prm.Raw
|
cleaned := []byte(prm.Raw)
|
||||||
cleaned, _ = sjson.Delete(cleaned, "$schema")
|
cleaned, _ = sjson.DeleteBytes(cleaned, "$schema")
|
||||||
cleaned, _ = sjson.Set(cleaned, "additionalProperties", false)
|
cleaned, _ = sjson.SetBytes(cleaned, "additionalProperties", false)
|
||||||
tool, _ = sjson.SetRaw(tool, "parameters", cleaned)
|
tool, _ = sjson.SetRawBytes(tool, "parameters", cleaned)
|
||||||
}
|
}
|
||||||
tool, _ = sjson.Set(tool, "strict", false)
|
tool, _ = sjson.SetBytes(tool, "strict", false)
|
||||||
out, _ = sjson.SetRaw(out, "tools.-1", tool)
|
out, _ = sjson.SetRawBytes(out, "tools.-1", tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fixed flags aligning with Codex expectations
|
// 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.
|
// Convert Gemini thinkingConfig to Codex reasoning.effort.
|
||||||
// Note: Google official Python SDK sends snake_case fields (thinking_level/thinking_budget).
|
// 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() {
|
if thinkingLevel.Exists() {
|
||||||
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning.effort", effort)
|
out, _ = sjson.SetBytes(out, "reasoning.effort", effort)
|
||||||
effortSet = true
|
effortSet = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -263,7 +263,7 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
if thinkingBudget.Exists() {
|
if thinkingBudget.Exists() {
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
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
|
effortSet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,22 +272,22 @@ func ConvertGeminiRequestToCodex(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
}
|
}
|
||||||
if !effortSet {
|
if !effortSet {
|
||||||
// No thinking config, set default effort
|
// 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.SetBytes(out, "reasoning.summary", "auto")
|
||||||
out, _ = sjson.Set(out, "stream", true)
|
out, _ = sjson.SetBytes(out, "stream", true)
|
||||||
out, _ = sjson.Set(out, "store", false)
|
out, _ = sjson.SetBytes(out, "store", false)
|
||||||
out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"})
|
out, _ = sjson.SetBytes(out, "include", []string{"reasoning.encrypted_content"})
|
||||||
|
|
||||||
var pathsToLower []string
|
var pathsToLower []string
|
||||||
toolsResult := gjson.Get(out, "tools")
|
toolsResult := gjson.GetBytes(out, "tools")
|
||||||
util.Walk(toolsResult, "", "type", &pathsToLower)
|
util.Walk(toolsResult, "", "type", &pathsToLower)
|
||||||
for _, p := range pathsToLower {
|
for _, p := range pathsToLower {
|
||||||
fullPath := fmt.Sprintf("tools.%s", p)
|
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.
|
// shortenNameIfNeeded applies the simple shortening rule for a single name.
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -23,7 +23,7 @@ type ConvertCodexResponseToGeminiParams struct {
|
|||||||
Model string
|
Model string
|
||||||
CreatedAt int64
|
CreatedAt int64
|
||||||
ResponseID string
|
ResponseID string
|
||||||
LastStorageOutput string
|
LastStorageOutput []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCodexResponseToGemini converts Codex streaming response format to Gemini format.
|
// 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
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response
|
// - [][]byte: A slice of Gemini-compatible JSON responses
|
||||||
func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &ConvertCodexResponseToGeminiParams{
|
*param = &ConvertCodexResponseToGeminiParams{
|
||||||
Model: modelName,
|
Model: modelName,
|
||||||
CreatedAt: 0,
|
CreatedAt: 0,
|
||||||
ResponseID: "",
|
ResponseID: "",
|
||||||
LastStorageOutput: "",
|
LastStorageOutput: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
@@ -59,17 +59,17 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
|||||||
typeStr := typeResult.String()
|
typeStr := typeResult.String()
|
||||||
|
|
||||||
// Base Gemini response template
|
// 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"}`
|
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 (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput != "" && typeStr == "response.output_item.done" {
|
if len((*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput) > 0 && typeStr == "response.output_item.done" {
|
||||||
template = (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput
|
template = append([]byte(nil), (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput...)
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "modelVersion", (*param).(*ConvertCodexResponseToGeminiParams).Model)
|
template, _ = sjson.SetBytes(template, "modelVersion", (*param).(*ConvertCodexResponseToGeminiParams).Model)
|
||||||
createdAtResult := rootResult.Get("response.created_at")
|
createdAtResult := rootResult.Get("response.created_at")
|
||||||
if createdAtResult.Exists() {
|
if createdAtResult.Exists() {
|
||||||
(*param).(*ConvertCodexResponseToGeminiParams).CreatedAt = createdAtResult.Int()
|
(*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
|
// Handle function call completion
|
||||||
@@ -78,7 +78,7 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
|||||||
itemType := itemResult.Get("type").String()
|
itemType := itemResult.Get("type").String()
|
||||||
if itemType == "function_call" {
|
if itemType == "function_call" {
|
||||||
// Create function call part
|
// Create function call part
|
||||||
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`)
|
||||||
{
|
{
|
||||||
// Restore original tool name if shortened
|
// Restore original tool name if shortened
|
||||||
n := itemResult.Get("name").String()
|
n := itemResult.Get("name").String()
|
||||||
@@ -86,7 +86,7 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
|||||||
if orig, ok := rev[n]; ok {
|
if orig, ok := rev[n]; ok {
|
||||||
n = orig
|
n = orig
|
||||||
}
|
}
|
||||||
functionCall, _ = sjson.Set(functionCall, "functionCall.name", n)
|
functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and set arguments
|
// Parse and set arguments
|
||||||
@@ -94,47 +94,48 @@ func ConvertCodexResponseToGemini(_ context.Context, modelName string, originalR
|
|||||||
if argsStr != "" {
|
if argsStr != "" {
|
||||||
argsResult := gjson.Parse(argsStr)
|
argsResult := gjson.Parse(argsStr)
|
||||||
if argsResult.IsObject() {
|
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.SetRawBytes(template, "candidates.0.content.parts.-1", functionCall)
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
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
|
// Use this return to storage message
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if typeStr == "response.created" { // Handle response creation - set model and response ID
|
if typeStr == "response.created" { // Handle response creation - set model and response ID
|
||||||
template, _ = sjson.Set(template, "modelVersion", rootResult.Get("response.model").String())
|
template, _ = sjson.SetBytes(template, "modelVersion", rootResult.Get("response.model").String())
|
||||||
template, _ = sjson.Set(template, "responseId", rootResult.Get("response.id").String())
|
template, _ = sjson.SetBytes(template, "responseId", rootResult.Get("response.id").String())
|
||||||
(*param).(*ConvertCodexResponseToGeminiParams).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
|
} else if typeStr == "response.reasoning_summary_text.delta" { // Handle reasoning/thinking content delta
|
||||||
part := `{"thought":true,"text":""}`
|
part := []byte(`{"thought":true,"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", rootResult.Get("delta").String())
|
part, _ = sjson.SetBytes(part, "text", rootResult.Get("delta").String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||||
} else if typeStr == "response.output_text.delta" { // Handle regular text content delta
|
} else if typeStr == "response.output_text.delta" { // Handle regular text content delta
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", rootResult.Get("delta").String())
|
part, _ = sjson.SetBytes(part, "text", rootResult.Get("delta").String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||||
} else if typeStr == "response.completed" { // Handle response completion with usage metadata
|
} 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.SetBytes(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.candidatesTokenCount", rootResult.Get("response.usage.output_tokens").Int())
|
||||||
totalTokens := rootResult.Get("response.usage.input_tokens").Int() + 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 {
|
} else {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput != "" {
|
if len((*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput) > 0 {
|
||||||
return []string{(*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput, template}
|
return [][]byte{
|
||||||
} else {
|
append([]byte(nil), (*param).(*ConvertCodexResponseToGeminiParams).LastStorageOutput...),
|
||||||
return []string{template}
|
template,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return [][]byte{template}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCodexResponseToGeminiNonStream converts a non-streaming Codex response to a non-streaming Gemini response.
|
// 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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response containing all message content and metadata
|
// - []byte: A Gemini-compatible JSON response containing all message content and metadata
|
||||||
func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
rootResult := gjson.ParseBytes(rawJSON)
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Verify this is a response.completed event
|
// Verify this is a response.completed event
|
||||||
if rootResult.Get("type").String() != "response.completed" {
|
if rootResult.Get("type").String() != "response.completed" {
|
||||||
return ""
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base Gemini response template for non-streaming
|
// 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
|
// Set model version
|
||||||
template, _ = sjson.Set(template, "modelVersion", modelName)
|
template, _ = sjson.SetBytes(template, "modelVersion", modelName)
|
||||||
|
|
||||||
// Set response metadata from the completed response
|
// Set response metadata from the completed response
|
||||||
responseData := rootResult.Get("response")
|
responseData := rootResult.Get("response")
|
||||||
if responseData.Exists() {
|
if responseData.Exists() {
|
||||||
// Set response ID
|
// Set response ID
|
||||||
if responseId := responseData.Get("id"); responseId.Exists() {
|
if responseId := responseData.Get("id"); responseId.Exists() {
|
||||||
template, _ = sjson.Set(template, "responseId", responseId.String())
|
template, _ = sjson.SetBytes(template, "responseId", responseId.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set creation time
|
// Set creation time
|
||||||
if createdAt := responseData.Get("created_at"); createdAt.Exists() {
|
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
|
// Set usage metadata
|
||||||
@@ -183,14 +184,14 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
outputTokens := usage.Get("output_tokens").Int()
|
outputTokens := usage.Get("output_tokens").Int()
|
||||||
totalTokens := inputTokens + outputTokens
|
totalTokens := inputTokens + outputTokens
|
||||||
|
|
||||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", inputTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", inputTokens)
|
||||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", outputTokens)
|
||||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", totalTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", totalTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process output content to build parts array
|
// Process output content to build parts array
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
var pendingFunctionCalls []string
|
var pendingFunctionCalls [][]byte
|
||||||
|
|
||||||
flushPendingFunctionCalls := func() {
|
flushPendingFunctionCalls := func() {
|
||||||
if len(pendingFunctionCalls) == 0 {
|
if len(pendingFunctionCalls) == 0 {
|
||||||
@@ -199,7 +200,7 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
// Add all pending function calls as individual parts
|
// Add all pending function calls as individual parts
|
||||||
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
// This maintains the original Gemini API format while ensuring consecutive calls are grouped together
|
||||||
for _, fc := range pendingFunctionCalls {
|
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
|
pendingFunctionCalls = nil
|
||||||
}
|
}
|
||||||
@@ -215,9 +216,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
|
|
||||||
// Add thinking content
|
// Add thinking content
|
||||||
if content := value.Get("content"); content.Exists() {
|
if content := value.Get("content"); content.Exists() {
|
||||||
part := `{"text":"","thought":true}`
|
part := []byte(`{"text":"","thought":true}`)
|
||||||
part, _ = sjson.Set(part, "text", content.String())
|
part, _ = sjson.SetBytes(part, "text", content.String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "message":
|
case "message":
|
||||||
@@ -229,9 +230,9 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
content.ForEach(func(_, contentItem gjson.Result) bool {
|
content.ForEach(func(_, contentItem gjson.Result) bool {
|
||||||
if contentItem.Get("type").String() == "output_text" {
|
if contentItem.Get("type").String() == "output_text" {
|
||||||
if text := contentItem.Get("text"); text.Exists() {
|
if text := contentItem.Get("text"); text.Exists() {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", text.String())
|
part, _ = sjson.SetBytes(part, "text", text.String())
|
||||||
template, _ = sjson.SetRaw(template, "candidates.0.content.parts.-1", part)
|
template, _ = sjson.SetRawBytes(template, "candidates.0.content.parts.-1", part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -241,21 +242,21 @@ func ConvertCodexResponseToGeminiNonStream(_ context.Context, modelName string,
|
|||||||
case "function_call":
|
case "function_call":
|
||||||
// Collect function call for potential merging with consecutive ones
|
// Collect function call for potential merging with consecutive ones
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
functionCall := `{"functionCall":{"args":{},"name":""}}`
|
functionCall := []byte(`{"functionCall":{"args":{},"name":""}}`)
|
||||||
{
|
{
|
||||||
n := value.Get("name").String()
|
n := value.Get("name").String()
|
||||||
rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
|
rev := buildReverseMapFromGeminiOriginal(originalRequestRawJSON)
|
||||||
if orig, ok := rev[n]; ok {
|
if orig, ok := rev[n]; ok {
|
||||||
n = orig
|
n = orig
|
||||||
}
|
}
|
||||||
functionCall, _ = sjson.Set(functionCall, "functionCall.name", n)
|
functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and set arguments
|
// Parse and set arguments
|
||||||
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
if argsStr := value.Get("arguments").String(); argsStr != "" {
|
||||||
argsResult := gjson.Parse(argsStr)
|
argsResult := gjson.Parse(argsStr)
|
||||||
if argsResult.IsObject() {
|
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
|
// Set finish reason based on whether there were tool calls
|
||||||
if hasToolCall {
|
if hasToolCall {
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "candidates.0.finishReason", "STOP")
|
template, _ = sjson.SetBytes(template, "candidates.0.finishReason", "STOP")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return template
|
return template
|
||||||
@@ -307,6 +308,6 @@ func buildReverseMapFromGeminiOriginal(original []byte) map[string]string {
|
|||||||
return rev
|
return rev
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,42 +29,42 @@ import (
|
|||||||
func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
// Start with empty JSON object
|
// Start with empty JSON object
|
||||||
out := `{"instructions":""}`
|
out := []byte(`{"instructions":""}`)
|
||||||
|
|
||||||
// Stream must be set to true
|
// 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
|
// Codex not support temperature, top_p, top_k, max_output_tokens, so comment them
|
||||||
// if v := gjson.GetBytes(rawJSON, "temperature"); v.Exists() {
|
// 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() {
|
// 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() {
|
// 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
|
// Map token limits
|
||||||
// if v := gjson.GetBytes(rawJSON, "max_tokens"); v.Exists() {
|
// 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() {
|
// 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
|
// Map reasoning effort
|
||||||
if v := gjson.GetBytes(rawJSON, "reasoning_effort"); v.Exists() {
|
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 {
|
} 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.SetBytes(out, "parallel_tool_calls", true)
|
||||||
out, _ = sjson.Set(out, "reasoning.summary", "auto")
|
out, _ = sjson.SetBytes(out, "reasoning.summary", "auto")
|
||||||
out, _ = sjson.Set(out, "include", []string{"reasoning.encrypted_content"})
|
out, _ = sjson.SetBytes(out, "include", []string{"reasoning.encrypted_content"})
|
||||||
|
|
||||||
// Model
|
// Model
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// Build tool name shortening map from original tools (if any)
|
// Build tool name shortening map from original tools (if any)
|
||||||
originalToolNameMap := map[string]string{}
|
originalToolNameMap := map[string]string{}
|
||||||
@@ -100,9 +100,9 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
// if m.Get("role").String() == "system" {
|
// if m.Get("role").String() == "system" {
|
||||||
// c := m.Get("content")
|
// c := m.Get("content")
|
||||||
// if c.Type == gjson.String {
|
// 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" {
|
// } 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
|
// break
|
||||||
// }
|
// }
|
||||||
@@ -110,7 +110,7 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Build input from messages, handling all message types including tool calls
|
// 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() {
|
if messages.IsArray() {
|
||||||
arr := messages.Array()
|
arr := messages.Array()
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
@@ -124,23 +124,23 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
content := m.Get("content").String()
|
content := m.Get("content").String()
|
||||||
|
|
||||||
// Create function_call_output object
|
// Create function_call_output object
|
||||||
funcOutput := `{}`
|
funcOutput := []byte(`{}`)
|
||||||
funcOutput, _ = sjson.Set(funcOutput, "type", "function_call_output")
|
funcOutput, _ = sjson.SetBytes(funcOutput, "type", "function_call_output")
|
||||||
funcOutput, _ = sjson.Set(funcOutput, "call_id", toolCallID)
|
funcOutput, _ = sjson.SetBytes(funcOutput, "call_id", toolCallID)
|
||||||
funcOutput, _ = sjson.Set(funcOutput, "output", content)
|
funcOutput, _ = sjson.SetBytes(funcOutput, "output", content)
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", funcOutput)
|
out, _ = sjson.SetRawBytes(out, "input.-1", funcOutput)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Handle regular messages
|
// Handle regular messages
|
||||||
msg := `{}`
|
msg := []byte(`{}`)
|
||||||
msg, _ = sjson.Set(msg, "type", "message")
|
msg, _ = sjson.SetBytes(msg, "type", "message")
|
||||||
if role == "system" {
|
if role == "system" {
|
||||||
msg, _ = sjson.Set(msg, "role", "developer")
|
msg, _ = sjson.SetBytes(msg, "role", "developer")
|
||||||
} else {
|
} 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
|
// Handle regular content
|
||||||
c := m.Get("content")
|
c := m.Get("content")
|
||||||
@@ -150,10 +150,10 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
partType = "output_text"
|
partType = "output_text"
|
||||||
}
|
}
|
||||||
part := `{}`
|
part := []byte(`{}`)
|
||||||
part, _ = sjson.Set(part, "type", partType)
|
part, _ = sjson.SetBytes(part, "type", partType)
|
||||||
part, _ = sjson.Set(part, "text", c.String())
|
part, _ = sjson.SetBytes(part, "text", c.String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", part)
|
||||||
} else if c.Exists() && c.IsArray() {
|
} else if c.Exists() && c.IsArray() {
|
||||||
items := c.Array()
|
items := c.Array()
|
||||||
for j := 0; j < len(items); j++ {
|
for j := 0; j < len(items); j++ {
|
||||||
@@ -165,32 +165,32 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
partType = "output_text"
|
partType = "output_text"
|
||||||
}
|
}
|
||||||
part := `{}`
|
part := []byte(`{}`)
|
||||||
part, _ = sjson.Set(part, "type", partType)
|
part, _ = sjson.SetBytes(part, "type", partType)
|
||||||
part, _ = sjson.Set(part, "text", it.Get("text").String())
|
part, _ = sjson.SetBytes(part, "text", it.Get("text").String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", part)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", part)
|
||||||
case "image_url":
|
case "image_url":
|
||||||
// Map image inputs to input_image for Responses API
|
// Map image inputs to input_image for Responses API
|
||||||
if role == "user" {
|
if role == "user" {
|
||||||
part := `{}`
|
part := []byte(`{}`)
|
||||||
part, _ = sjson.Set(part, "type", "input_image")
|
part, _ = sjson.SetBytes(part, "type", "input_image")
|
||||||
if u := it.Get("image_url.url"); u.Exists() {
|
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":
|
case "file":
|
||||||
if role == "user" {
|
if role == "user" {
|
||||||
fileData := it.Get("file.file_data").String()
|
fileData := it.Get("file.file_data").String()
|
||||||
filename := it.Get("file.filename").String()
|
filename := it.Get("file.filename").String()
|
||||||
if fileData != "" {
|
if fileData != "" {
|
||||||
part := `{}`
|
part := []byte(`{}`)
|
||||||
part, _ = sjson.Set(part, "type", "input_file")
|
part, _ = sjson.SetBytes(part, "type", "input_file")
|
||||||
part, _ = sjson.Set(part, "file_data", fileData)
|
part, _ = sjson.SetBytes(part, "file_data", fileData)
|
||||||
if filename != "" {
|
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
|
// Don't emit empty assistant messages when only tool_calls
|
||||||
// are present — Responses API needs function_call items
|
// are present — Responses API needs function_call items
|
||||||
// directly, otherwise call_id matching fails (#2132).
|
// directly, otherwise call_id matching fails (#2132).
|
||||||
if role != "assistant" || len(gjson.Get(msg, "content").Array()) > 0 {
|
if role != "assistant" || len(gjson.GetBytes(msg, "content").Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "input.-1", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tool calls for assistant messages as separate top-level objects
|
// 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]
|
tc := toolCallsArr[j]
|
||||||
if tc.Get("type").String() == "function" {
|
if tc.Get("type").String() == "function" {
|
||||||
// Create function_call as top-level object
|
// Create function_call as top-level object
|
||||||
funcCall := `{}`
|
funcCall := []byte(`{}`)
|
||||||
funcCall, _ = sjson.Set(funcCall, "type", "function_call")
|
funcCall, _ = sjson.SetBytes(funcCall, "type", "function_call")
|
||||||
funcCall, _ = sjson.Set(funcCall, "call_id", tc.Get("id").String())
|
funcCall, _ = sjson.SetBytes(funcCall, "call_id", tc.Get("id").String())
|
||||||
{
|
{
|
||||||
name := tc.Get("function.name").String()
|
name := tc.Get("function.name").String()
|
||||||
if short, ok := originalToolNameMap[name]; ok {
|
if short, ok := originalToolNameMap[name]; ok {
|
||||||
@@ -223,10 +223,10 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
} else {
|
} else {
|
||||||
name = shortenNameIfNeeded(name)
|
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())
|
funcCall, _ = sjson.SetBytes(funcCall, "arguments", tc.Get("function.arguments").String())
|
||||||
out, _ = sjson.SetRaw(out, "input.-1", funcCall)
|
out, _ = sjson.SetRawBytes(out, "input.-1", funcCall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -240,26 +240,26 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
text := gjson.GetBytes(rawJSON, "text")
|
text := gjson.GetBytes(rawJSON, "text")
|
||||||
if rf.Exists() {
|
if rf.Exists() {
|
||||||
// Always create text object when response_format provided
|
// Always create text object when response_format provided
|
||||||
if !gjson.Get(out, "text").Exists() {
|
if !gjson.GetBytes(out, "text").Exists() {
|
||||||
out, _ = sjson.SetRaw(out, "text", `{}`)
|
out, _ = sjson.SetRawBytes(out, "text", []byte(`{}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
rft := rf.Get("type").String()
|
rft := rf.Get("type").String()
|
||||||
switch rft {
|
switch rft {
|
||||||
case "text":
|
case "text":
|
||||||
out, _ = sjson.Set(out, "text.format.type", "text")
|
out, _ = sjson.SetBytes(out, "text.format.type", "text")
|
||||||
case "json_schema":
|
case "json_schema":
|
||||||
js := rf.Get("json_schema")
|
js := rf.Get("json_schema")
|
||||||
if js.Exists() {
|
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() {
|
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() {
|
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() {
|
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
|
// Map verbosity if provided
|
||||||
if text.Exists() {
|
if text.Exists() {
|
||||||
if v := text.Get("verbosity"); v.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() {
|
} else if text.Exists() {
|
||||||
// If only text.verbosity present (no response_format), map verbosity
|
// If only text.verbosity present (no response_format), map verbosity
|
||||||
if v := text.Get("verbosity"); v.Exists() {
|
if v := text.Get("verbosity"); v.Exists() {
|
||||||
if !gjson.Get(out, "text").Exists() {
|
if !gjson.GetBytes(out, "text").Exists() {
|
||||||
out, _ = sjson.SetRaw(out, "text", `{}`)
|
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)
|
// Map tools (flatten function fields)
|
||||||
tools := gjson.GetBytes(rawJSON, "tools")
|
tools := gjson.GetBytes(rawJSON, "tools")
|
||||||
if tools.IsArray() && len(tools.Array()) > 0 {
|
if tools.IsArray() && len(tools.Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "tools", `[]`)
|
out, _ = sjson.SetRawBytes(out, "tools", []byte(`[]`))
|
||||||
arr := tools.Array()
|
arr := tools.Array()
|
||||||
for i := 0; i < len(arr); i++ {
|
for i := 0; i < len(arr); i++ {
|
||||||
t := 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.
|
// 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".
|
// Only "function" needs structural conversion because Chat Completions nests details under "function".
|
||||||
if toolType != "" && toolType != "function" && t.IsObject() {
|
if toolType != "" && toolType != "function" && t.IsObject() {
|
||||||
out, _ = sjson.SetRaw(out, "tools.-1", t.Raw)
|
out, _ = sjson.SetRawBytes(out, "tools.-1", []byte(t.Raw))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if toolType == "function" {
|
if toolType == "function" {
|
||||||
item := `{}`
|
item := []byte(`{}`)
|
||||||
item, _ = sjson.Set(item, "type", "function")
|
item, _ = sjson.SetBytes(item, "type", "function")
|
||||||
fn := t.Get("function")
|
fn := t.Get("function")
|
||||||
if fn.Exists() {
|
if fn.Exists() {
|
||||||
if v := fn.Get("name"); v.Exists() {
|
if v := fn.Get("name"); v.Exists() {
|
||||||
@@ -307,19 +307,19 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
} else {
|
} else {
|
||||||
name = shortenNameIfNeeded(name)
|
name = shortenNameIfNeeded(name)
|
||||||
}
|
}
|
||||||
item, _ = sjson.Set(item, "name", name)
|
item, _ = sjson.SetBytes(item, "name", name)
|
||||||
}
|
}
|
||||||
if v := fn.Get("description"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
if tc := gjson.GetBytes(rawJSON, "tool_choice"); tc.Exists() {
|
||||||
switch {
|
switch {
|
||||||
case tc.Type == gjson.String:
|
case tc.Type == gjson.String:
|
||||||
out, _ = sjson.Set(out, "tool_choice", tc.String())
|
out, _ = sjson.SetBytes(out, "tool_choice", tc.String())
|
||||||
case tc.IsObject():
|
case tc.IsObject():
|
||||||
tcType := tc.Get("type").String()
|
tcType := tc.Get("type").String()
|
||||||
if tcType == "function" {
|
if tcType == "function" {
|
||||||
@@ -342,21 +342,21 @@ func ConvertOpenAIRequestToCodex(modelName string, inputRawJSON []byte, stream b
|
|||||||
name = shortenNameIfNeeded(name)
|
name = shortenNameIfNeeded(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
choice := `{}`
|
choice := []byte(`{}`)
|
||||||
choice, _ = sjson.Set(choice, "type", "function")
|
choice, _ = sjson.SetBytes(choice, "type", "function")
|
||||||
if name != "" {
|
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 != "" {
|
} else if tcType != "" {
|
||||||
// Built-in tool choices (e.g. {"type":"web_search"}) are already Responses-compatible.
|
// 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)
|
out, _ = sjson.SetBytes(out, "store", false)
|
||||||
return []byte(out)
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortenNameIfNeeded applies the simple shortening rule for a single name.
|
// shortenNameIfNeeded applies the simple shortening rule for a single name.
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ type ConvertCliToOpenAIParams struct {
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
// - [][]byte: A slice of OpenAI-compatible JSON responses
|
||||||
func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &ConvertCliToOpenAIParams{
|
*param = &ConvertCliToOpenAIParams{
|
||||||
Model: modelName,
|
Model: modelName,
|
||||||
@@ -55,12 +55,12 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
// Initialize the OpenAI SSE template.
|
// 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)
|
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).ResponseID = rootResult.Get("response.id").String()
|
||||||
(*param).(*ConvertCliToOpenAIParams).CreatedAt = rootResult.Get("response.created_at").Int()
|
(*param).(*ConvertCliToOpenAIParams).CreatedAt = rootResult.Get("response.created_at").Int()
|
||||||
(*param).(*ConvertCliToOpenAIParams).Model = rootResult.Get("response.model").String()
|
(*param).(*ConvertCliToOpenAIParams).Model = rootResult.Get("response.model").String()
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set the model version.
|
// Extract and set the model version.
|
||||||
cachedModel := (*param).(*ConvertCliToOpenAIParams).Model
|
cachedModel := (*param).(*ConvertCliToOpenAIParams).Model
|
||||||
if modelResult := gjson.GetBytes(rawJSON, "model"); modelResult.Exists() {
|
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 != "" {
|
} else if cachedModel != "" {
|
||||||
template, _ = sjson.Set(template, "model", cachedModel)
|
template, _ = sjson.SetBytes(template, "model", cachedModel)
|
||||||
} else if modelName != "" {
|
} 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.
|
// 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).
|
// Extract and set usage metadata (token counts).
|
||||||
if usageResult := gjson.GetBytes(rawJSON, "response.usage"); usageResult.Exists() {
|
if usageResult := gjson.GetBytes(rawJSON, "response.usage"); usageResult.Exists() {
|
||||||
if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.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() {
|
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() {
|
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() {
|
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() {
|
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 dataType == "response.reasoning_summary_text.delta" {
|
||||||
if deltaResult := rootResult.Get("delta"); deltaResult.Exists() {
|
if deltaResult := rootResult.Get("delta"); deltaResult.Exists() {
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", deltaResult.String())
|
template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", deltaResult.String())
|
||||||
}
|
}
|
||||||
} else if dataType == "response.reasoning_summary_text.done" {
|
} else if dataType == "response.reasoning_summary_text.done" {
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", "\n\n")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.reasoning_content", "\n\n")
|
||||||
} else if dataType == "response.output_text.delta" {
|
} else if dataType == "response.output_text.delta" {
|
||||||
if deltaResult := rootResult.Get("delta"); deltaResult.Exists() {
|
if deltaResult := rootResult.Get("delta"); deltaResult.Exists() {
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.content", deltaResult.String())
|
template, _ = sjson.SetBytes(template, "choices.0.delta.content", deltaResult.String())
|
||||||
}
|
}
|
||||||
} else if dataType == "response.completed" {
|
} else if dataType == "response.completed" {
|
||||||
finishReason := "stop"
|
finishReason := "stop"
|
||||||
if (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex != -1 {
|
if (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex != -1 {
|
||||||
finishReason = "tool_calls"
|
finishReason = "tool_calls"
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason)
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason)
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReason)
|
template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", finishReason)
|
||||||
} else if dataType == "response.output_item.added" {
|
} else if dataType == "response.output_item.added" {
|
||||||
itemResult := rootResult.Get("item")
|
itemResult := rootResult.Get("item")
|
||||||
if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" {
|
if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment index for this new function call item.
|
// 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).HasReceivedArgumentsDelta = false
|
||||||
(*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced = true
|
(*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced = true
|
||||||
|
|
||||||
functionCallItemTemplate := `{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}`
|
functionCallItemTemplate := []byte(`{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", itemResult.Get("call_id").String())
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", itemResult.Get("call_id").String())
|
||||||
|
|
||||||
// Restore original tool name if it was shortened.
|
// Restore original tool name if it was shortened.
|
||||||
name := itemResult.Get("name").String()
|
name := itemResult.Get("name").String()
|
||||||
@@ -148,59 +148,59 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR
|
|||||||
if orig, ok := rev[name]; ok {
|
if orig, ok := rev[name]; ok {
|
||||||
name = orig
|
name = orig
|
||||||
}
|
}
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", name)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.name", name)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", "")
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", "")
|
||||||
|
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`))
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
||||||
|
|
||||||
} else if dataType == "response.function_call_arguments.delta" {
|
} else if dataType == "response.function_call_arguments.delta" {
|
||||||
(*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta = true
|
(*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta = true
|
||||||
|
|
||||||
deltaValue := rootResult.Get("delta").String()
|
deltaValue := rootResult.Get("delta").String()
|
||||||
functionCallItemTemplate := `{"index":0,"function":{"arguments":""}}`
|
functionCallItemTemplate := []byte(`{"index":0,"function":{"arguments":""}}`)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", deltaValue)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", deltaValue)
|
||||||
|
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`))
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
||||||
|
|
||||||
} else if dataType == "response.function_call_arguments.done" {
|
} else if dataType == "response.function_call_arguments.done" {
|
||||||
if (*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta {
|
if (*param).(*ConvertCliToOpenAIParams).HasReceivedArgumentsDelta {
|
||||||
// Arguments were already streamed via delta events; nothing to emit.
|
// 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.
|
// Fallback: no delta events were received, emit the full arguments as a single chunk.
|
||||||
fullArgs := rootResult.Get("arguments").String()
|
fullArgs := rootResult.Get("arguments").String()
|
||||||
functionCallItemTemplate := `{"index":0,"function":{"arguments":""}}`
|
functionCallItemTemplate := []byte(`{"index":0,"function":{"arguments":""}}`)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fullArgs)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", fullArgs)
|
||||||
|
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`))
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
||||||
|
|
||||||
} else if dataType == "response.output_item.done" {
|
} else if dataType == "response.output_item.done" {
|
||||||
itemResult := rootResult.Get("item")
|
itemResult := rootResult.Get("item")
|
||||||
if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" {
|
if !itemResult.Exists() || itemResult.Get("type").String() != "function_call" {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced {
|
if (*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced {
|
||||||
// Tool call was already announced via output_item.added; skip emission.
|
// Tool call was already announced via output_item.added; skip emission.
|
||||||
(*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced = false
|
(*param).(*ConvertCliToOpenAIParams).HasToolCallAnnounced = false
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback path: model skipped output_item.added, so emit complete tool call now.
|
// Fallback path: model skipped output_item.added, so emit complete tool call now.
|
||||||
(*param).(*ConvertCliToOpenAIParams).FunctionCallIndex++
|
(*param).(*ConvertCliToOpenAIParams).FunctionCallIndex++
|
||||||
|
|
||||||
functionCallItemTemplate := `{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}`
|
functionCallItemTemplate := []byte(`{"index":0,"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "index", (*param).(*ConvertCliToOpenAIParams).FunctionCallIndex)
|
||||||
|
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", `[]`)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls", []byte(`[]`))
|
||||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", itemResult.Get("call_id").String())
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "id", itemResult.Get("call_id").String())
|
||||||
|
|
||||||
// Restore original tool name if it was shortened.
|
// Restore original tool name if it was shortened.
|
||||||
name := itemResult.Get("name").String()
|
name := itemResult.Get("name").String()
|
||||||
@@ -208,17 +208,17 @@ func ConvertCodexResponseToOpenAI(_ context.Context, modelName string, originalR
|
|||||||
if orig, ok := rev[name]; ok {
|
if orig, ok := rev[name]; ok {
|
||||||
name = orig
|
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())
|
functionCallItemTemplate, _ = sjson.SetBytes(functionCallItemTemplate, "function.arguments", itemResult.Get("arguments").String())
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallItemTemplate)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{template}
|
return [][]byte{template}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertCodexResponseToOpenAINonStream converts a non-streaming Codex response to a non-streaming OpenAI response.
|
// 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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []byte: An OpenAI-compatible JSON response containing all message content and metadata
|
||||||
func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
rootResult := gjson.ParseBytes(rawJSON)
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
// Verify this is a response.completed event
|
// Verify this is a response.completed event
|
||||||
if rootResult.Get("type").String() != "response.completed" {
|
if rootResult.Get("type").String() != "response.completed" {
|
||||||
return ""
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
unixTimestamp := time.Now().Unix()
|
unixTimestamp := time.Now().Unix()
|
||||||
|
|
||||||
responseResult := rootResult.Get("response")
|
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.
|
// Extract and set the model version.
|
||||||
if modelResult := responseResult.Get("model"); modelResult.Exists() {
|
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.
|
// Extract and set the creation timestamp.
|
||||||
if createdAtResult := responseResult.Get("created_at"); createdAtResult.Exists() {
|
if createdAtResult := responseResult.Get("created_at"); createdAtResult.Exists() {
|
||||||
template, _ = sjson.Set(template, "created", createdAtResult.Int())
|
template, _ = sjson.SetBytes(template, "created", createdAtResult.Int())
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", unixTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set the response ID.
|
// Extract and set the response ID.
|
||||||
if idResult := responseResult.Get("id"); idResult.Exists() {
|
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).
|
// Extract and set usage metadata (token counts).
|
||||||
if usageResult := responseResult.Get("usage"); usageResult.Exists() {
|
if usageResult := responseResult.Get("usage"); usageResult.Exists() {
|
||||||
if outputTokensResult := usageResult.Get("output_tokens"); outputTokensResult.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() {
|
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() {
|
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() {
|
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() {
|
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()
|
outputArray := outputResult.Array()
|
||||||
var contentText string
|
var contentText string
|
||||||
var reasoningText string
|
var reasoningText string
|
||||||
var toolCalls []string
|
var toolCalls [][]byte
|
||||||
|
|
||||||
for _, outputItem := range outputArray {
|
for _, outputItem := range outputArray {
|
||||||
outputType := outputItem.Get("type").String()
|
outputType := outputItem.Get("type").String()
|
||||||
@@ -319,10 +319,10 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original
|
|||||||
}
|
}
|
||||||
case "function_call":
|
case "function_call":
|
||||||
// Handle function call content
|
// 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() {
|
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() {
|
if nameResult := outputItem.Get("name"); nameResult.Exists() {
|
||||||
@@ -331,11 +331,11 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original
|
|||||||
if orig, ok := rev[n]; ok {
|
if orig, ok := rev[n]; ok {
|
||||||
n = orig
|
n = orig
|
||||||
}
|
}
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", n)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
if argsResult := outputItem.Get("arguments"); argsResult.Exists() {
|
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)
|
toolCalls = append(toolCalls, functionCallTemplate)
|
||||||
@@ -344,22 +344,22 @@ func ConvertCodexResponseToOpenAINonStream(_ context.Context, _ string, original
|
|||||||
|
|
||||||
// Set content and reasoning content if found
|
// Set content and reasoning content if found
|
||||||
if contentText != "" {
|
if contentText != "" {
|
||||||
template, _ = sjson.Set(template, "choices.0.message.content", contentText)
|
template, _ = sjson.SetBytes(template, "choices.0.message.content", contentText)
|
||||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant")
|
||||||
}
|
}
|
||||||
|
|
||||||
if reasoningText != "" {
|
if reasoningText != "" {
|
||||||
template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningText)
|
template, _ = sjson.SetBytes(template, "choices.0.message.reasoning_content", reasoningText)
|
||||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.message.role", "assistant")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add tool calls if any
|
// Add tool calls if any
|
||||||
if len(toolCalls) > 0 {
|
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 {
|
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() {
|
if statusResult := responseResult.Get("status"); statusResult.Exists() {
|
||||||
status := statusResult.String()
|
status := statusResult.String()
|
||||||
if status == "completed" {
|
if status == "completed" {
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "stop")
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", "stop")
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "stop")
|
template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", "stop")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ func TestConvertCodexResponseToOpenAI_StreamSetsModelFromResponseCreated(t *test
|
|||||||
t.Fatalf("expected 1 chunk, got %d", len(out))
|
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 {
|
if gotModel != modelName {
|
||||||
t.Fatalf("expected model %q, got %q", modelName, gotModel)
|
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))
|
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 {
|
if gotModel != modelName {
|
||||||
t.Fatalf("expected model %q, got %q", modelName, gotModel)
|
t.Fatalf("expected model %q, got %q", modelName, gotModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte,
|
|||||||
|
|
||||||
inputResult := gjson.GetBytes(rawJSON, "input")
|
inputResult := gjson.GetBytes(rawJSON, "input")
|
||||||
if inputResult.Type == gjson.String {
|
if inputResult.Type == gjson.String {
|
||||||
input, _ := sjson.Set(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`, "0.content.0.text", inputResult.String())
|
input, _ := sjson.SetBytes([]byte(`[{"type":"message","role":"user","content":[{"type":"input_text","text":""}]}]`), "0.content.0.text", inputResult.String())
|
||||||
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", []byte(input))
|
rawJSON, _ = sjson.SetRawBytes(rawJSON, "input", input)
|
||||||
}
|
}
|
||||||
|
|
||||||
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
|
rawJSON, _ = sjson.SetBytes(rawJSON, "stream", true)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package responses
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
@@ -11,23 +10,25 @@ import (
|
|||||||
// ConvertCodexResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
// ConvertCodexResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
||||||
// to OpenAI Responses SSE events (response.*).
|
// 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:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
out := fmt.Sprintf("data: %s", string(rawJSON))
|
out := make([]byte, 0, len(rawJSON)+len("data: "))
|
||||||
return []string{out}
|
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
|
// ConvertCodexResponseToOpenAIResponsesNonStream builds a single Responses JSON
|
||||||
// from a non-streaming OpenAI Chat Completions response.
|
// 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)
|
rootResult := gjson.ParseBytes(rawJSON)
|
||||||
// Verify this is a response.completed event
|
// Verify this is a response.completed event
|
||||||
if rootResult.Get("type").String() != "response.completed" {
|
if rootResult.Get("type").String() != "response.completed" {
|
||||||
return ""
|
return []byte{}
|
||||||
}
|
}
|
||||||
responseResult := rootResult.Get("response")
|
responseResult := rootResult.Get("response")
|
||||||
return responseResult.Raw
|
return []byte(responseResult.Raw)
|
||||||
}
|
}
|
||||||
|
|||||||
67
internal/translator/common/bytes.go
Normal file
67
internal/translator/common/bytes.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -38,30 +38,30 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
// Build output Gemini CLI request JSON
|
||||||
out := `{"model":"","request":{"contents":[]}}`
|
out := []byte(`{"model":"","request":{"contents":[]}}`)
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
|
if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
|
||||||
systemInstruction := `{"role":"user","parts":[]}`
|
systemInstruction := []byte(`{"role":"user","parts":[]}`)
|
||||||
hasSystemParts := false
|
hasSystemParts := false
|
||||||
systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
|
systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
|
||||||
if systemPromptResult.Get("type").String() == "text" {
|
if systemPromptResult.Get("type").String() == "text" {
|
||||||
textResult := systemPromptResult.Get("text")
|
textResult := systemPromptResult.Get("text")
|
||||||
if textResult.Type == gjson.String {
|
if textResult.Type == gjson.String {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", textResult.String())
|
part, _ = sjson.SetBytes(part, "text", textResult.String())
|
||||||
systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
|
systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part)
|
||||||
hasSystemParts = true
|
hasSystemParts = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if hasSystemParts {
|
if hasSystemParts {
|
||||||
out, _ = sjson.SetRaw(out, "request.systemInstruction", systemInstruction)
|
out, _ = sjson.SetRawBytes(out, "request.systemInstruction", systemInstruction)
|
||||||
}
|
}
|
||||||
} else if systemResult.Type == gjson.String {
|
} 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
|
// contents
|
||||||
@@ -76,28 +76,28 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
role = "model"
|
role = "model"
|
||||||
}
|
}
|
||||||
|
|
||||||
contentJSON := `{"role":"","parts":[]}`
|
contentJSON := []byte(`{"role":"","parts":[]}`)
|
||||||
contentJSON, _ = sjson.Set(contentJSON, "role", role)
|
contentJSON, _ = sjson.SetBytes(contentJSON, "role", role)
|
||||||
|
|
||||||
contentsResult := messageResult.Get("content")
|
contentsResult := messageResult.Get("content")
|
||||||
if contentsResult.IsArray() {
|
if contentsResult.IsArray() {
|
||||||
contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
|
contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
|
||||||
switch contentResult.Get("type").String() {
|
switch contentResult.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
|
part, _ = sjson.SetBytes(part, "text", contentResult.Get("text").String())
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
|
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
argsResult := gjson.Parse(functionArgs)
|
argsResult := gjson.Parse(functionArgs)
|
||||||
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
||||||
part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
|
part := []byte(`{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`)
|
||||||
part, _ = sjson.Set(part, "thoughtSignature", geminiCLIClaudeThoughtSignature)
|
part, _ = sjson.SetBytes(part, "thoughtSignature", geminiCLIClaudeThoughtSignature)
|
||||||
part, _ = sjson.Set(part, "functionCall.name", functionName)
|
part, _ = sjson.SetBytes(part, "functionCall.name", functionName)
|
||||||
part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
|
part, _ = sjson.SetRawBytes(part, "functionCall.args", []byte(functionArgs))
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
@@ -111,10 +111,10 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
funcName = strings.Join(toolCallIDs[0:len(toolCallIDs)-1], "-")
|
||||||
}
|
}
|
||||||
responseData := contentResult.Get("content").Raw
|
responseData := contentResult.Get("content").Raw
|
||||||
part := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
part := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
|
||||||
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
part, _ = sjson.SetBytes(part, "functionResponse.name", funcName)
|
||||||
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
part, _ = sjson.SetBytes(part, "functionResponse.response.result", responseData)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
|
|
||||||
case "image":
|
case "image":
|
||||||
source := contentResult.Get("source")
|
source := contentResult.Get("source")
|
||||||
@@ -122,21 +122,21 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
mimeType := source.Get("media_type").String()
|
mimeType := source.Get("media_type").String()
|
||||||
data := source.Get("data").String()
|
data := source.Get("data").String()
|
||||||
if mimeType != "" && data != "" {
|
if mimeType != "" && data != "" {
|
||||||
part := `{"inlineData":{"mime_type":"","data":""}}`
|
part := []byte(`{"inlineData":{"mime_type":"","data":""}}`)
|
||||||
part, _ = sjson.Set(part, "inlineData.mime_type", mimeType)
|
part, _ = sjson.SetBytes(part, "inlineData.mime_type", mimeType)
|
||||||
part, _ = sjson.Set(part, "inlineData.data", data)
|
part, _ = sjson.SetBytes(part, "inlineData.data", data)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", contentJSON)
|
||||||
} else if contentsResult.Type == gjson.String {
|
} else if contentsResult.Type == gjson.String {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", contentsResult.String())
|
part, _ = sjson.SetBytes(part, "text", contentsResult.String())
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
out, _ = sjson.SetRaw(out, "request.contents.-1", contentJSON)
|
out, _ = sjson.SetRawBytes(out, "request.contents.-1", contentJSON)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -149,26 +149,26 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := util.CleanJSONSchemaForGemini(inputSchemaResult.Raw)
|
inputSchema := util.CleanJSONSchemaForGemini(inputSchemaResult.Raw)
|
||||||
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
tool, _ := sjson.DeleteBytes([]byte(toolResult.Raw), "input_schema")
|
||||||
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
|
tool, _ = sjson.SetRawBytes(tool, "parametersJsonSchema", []byte(inputSchema))
|
||||||
tool, _ = sjson.Delete(tool, "strict")
|
tool, _ = sjson.DeleteBytes(tool, "strict")
|
||||||
tool, _ = sjson.Delete(tool, "input_examples")
|
tool, _ = sjson.DeleteBytes(tool, "input_examples")
|
||||||
tool, _ = sjson.Delete(tool, "type")
|
tool, _ = sjson.DeleteBytes(tool, "type")
|
||||||
tool, _ = sjson.Delete(tool, "cache_control")
|
tool, _ = sjson.DeleteBytes(tool, "cache_control")
|
||||||
tool, _ = sjson.Delete(tool, "defer_loading")
|
tool, _ = sjson.DeleteBytes(tool, "defer_loading")
|
||||||
tool, _ = sjson.Delete(tool, "eager_input_streaming")
|
tool, _ = sjson.DeleteBytes(tool, "eager_input_streaming")
|
||||||
if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
|
if gjson.ValidBytes(tool) && gjson.ParseBytes(tool).IsObject() {
|
||||||
if !hasTools {
|
if !hasTools {
|
||||||
out, _ = sjson.SetRaw(out, "request.tools", `[{"functionDeclarations":[]}]`)
|
out, _ = sjson.SetRawBytes(out, "request.tools", []byte(`[{"functionDeclarations":[]}]`))
|
||||||
hasTools = true
|
hasTools = true
|
||||||
}
|
}
|
||||||
out, _ = sjson.SetRaw(out, "request.tools.0.functionDeclarations.-1", tool)
|
out, _ = sjson.SetRawBytes(out, "request.tools.0.functionDeclarations.-1", tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if !hasTools {
|
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 {
|
switch toolChoiceType {
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "AUTO")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "AUTO")
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "NONE")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "NONE")
|
||||||
case "any":
|
case "any":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
case "tool":
|
case "tool":
|
||||||
out, _ = sjson.Set(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "request.toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
if toolChoiceName != "" {
|
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":
|
case "enabled":
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
budget := int(b.Int())
|
budget := int(b.Int())
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// For adaptive thinking:
|
// For adaptive thinking:
|
||||||
@@ -219,25 +219,23 @@ func ConvertClaudeRequestToCLI(modelName string, inputRawJSON []byte, _ bool) []
|
|||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort)
|
out, _ = sjson.SetBytes(out, "request.generationConfig.thinkingConfig.thinkingLevel", effort)
|
||||||
} else {
|
} 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 {
|
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 {
|
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 {
|
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)
|
out = common.AttachDefaultSafetySettings(out, "request.safetySettings")
|
||||||
outBytes = common.AttachDefaultSafetySettings(outBytes, "request.safetySettings")
|
return out
|
||||||
|
|
||||||
return outBytes
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -47,8 +48,8 @@ var toolUseIDCounter uint64
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Claude Code-compatible JSON response
|
// - [][]byte: A slice of bytes, each containing a Claude Code-compatible SSE payload.
|
||||||
func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &Params{
|
*param = &Params{
|
||||||
HasFirstResponse: false,
|
HasFirstResponse: false,
|
||||||
@@ -60,34 +61,33 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque
|
|||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
// Only send message_stop if we have actually output content
|
// Only send message_stop if we have actually output content
|
||||||
if (*param).(*Params).HasContent {
|
if (*param).(*Params).HasContent {
|
||||||
return []string{
|
return [][]byte{translatorcommon.AppendSSEEventString(nil, "message_stop", `{"type":"message_stop"}`, 3)}
|
||||||
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track whether tools are being used in this response chunk
|
// Track whether tools are being used in this response chunk
|
||||||
usedTool := false
|
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
|
// Initialize the streaming session with a message_start event
|
||||||
// This is only sent for the very first response chunk to establish the streaming session
|
// This is only sent for the very first response chunk to establish the streaming session
|
||||||
if !(*param).(*Params).HasFirstResponse {
|
if !(*param).(*Params).HasFirstResponse {
|
||||||
output = "event: message_start\n"
|
|
||||||
|
|
||||||
// Create the initial message structure with default values according to Claude Code API specification
|
// 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
|
// 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
|
// Override default values with actual response metadata if available from the Gemini CLI response
|
||||||
if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() {
|
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() {
|
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
|
(*param).(*Params).HasFirstResponse = true
|
||||||
}
|
}
|
||||||
@@ -110,9 +110,8 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque
|
|||||||
if partResult.Get("thought").Bool() {
|
if partResult.Get("thought").Bool() {
|
||||||
// Continue existing thinking block if already in thinking state
|
// Continue existing thinking block if already in thinking state
|
||||||
if (*param).(*Params).ResponseType == 2 {
|
if (*param).(*Params).ResponseType == 2 {
|
||||||
output = output + "event: content_block_delta\n"
|
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex)), "delta.thinking", partTextResult.String())
|
||||||
data, _ := sjson.Set(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))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
} else {
|
} else {
|
||||||
// Transition from another state to thinking
|
// 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 + 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 + "\n\n\n"
|
||||||
}
|
}
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new thinking content block
|
// Start a new thinking content block
|
||||||
output = output + "event: content_block_start\n"
|
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"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())
|
||||||
output = output + "\n\n\n"
|
appendEvent("content_block_delta", string(data))
|
||||||
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)
|
|
||||||
(*param).(*Params).ResponseType = 2 // Set state to thinking
|
(*param).(*Params).ResponseType = 2 // Set state to thinking
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
}
|
}
|
||||||
@@ -143,9 +137,8 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque
|
|||||||
// Process regular text content (user-visible output)
|
// Process regular text content (user-visible output)
|
||||||
// Continue existing text block if already in content state
|
// Continue existing text block if already in content state
|
||||||
if (*param).(*Params).ResponseType == 1 {
|
if (*param).(*Params).ResponseType == 1 {
|
||||||
output = output + "event: content_block_delta\n"
|
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex)), "delta.text", partTextResult.String())
|
||||||
data, _ := sjson.Set(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))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
} else {
|
} else {
|
||||||
// Transition from another state to text content
|
// 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 + 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 + "\n\n\n"
|
||||||
}
|
}
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new text content block
|
// Start a new text content block
|
||||||
output = output + "event: content_block_start\n"
|
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"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())
|
||||||
output = output + "\n\n\n"
|
appendEvent("content_block_delta", string(data))
|
||||||
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)
|
|
||||||
(*param).(*Params).ResponseType = 1 // Set state to content
|
(*param).(*Params).ResponseType = 1 // Set state to content
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
}
|
}
|
||||||
@@ -182,9 +170,7 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque
|
|||||||
// Handle state transitions when switching to function calls
|
// Handle state transitions when switching to function calls
|
||||||
// Close any existing function call block first
|
// Close any existing function call block first
|
||||||
if (*param).(*Params).ResponseType == 3 {
|
if (*param).(*Params).ResponseType == 3 {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
(*param).(*Params).ResponseType = 0
|
(*param).(*Params).ResponseType = 0
|
||||||
}
|
}
|
||||||
@@ -198,26 +184,21 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque
|
|||||||
|
|
||||||
// Close any other existing content block
|
// Close any other existing content block
|
||||||
if (*param).(*Params).ResponseType != 0 {
|
if (*param).(*Params).ResponseType != 0 {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new tool use content block
|
// Start a new tool use content block
|
||||||
// This creates the structure for a function call in Claude Code format
|
// 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
|
// 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 := []byte(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.SetBytes(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)
|
data, _ = sjson.SetBytes(data, "content_block.name", fcName)
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
appendEvent("content_block_start", string(data))
|
||||||
|
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
output = output + "event: content_block_delta\n"
|
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)
|
||||||
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)
|
appendEvent("content_block_delta", string(data))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
}
|
}
|
||||||
(*param).(*Params).ResponseType = 3
|
(*param).(*Params).ResponseType = 3
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
@@ -232,34 +213,28 @@ func ConvertGeminiCLIResponseToClaude(_ context.Context, _ string, originalReque
|
|||||||
// Only send final events if we have actually output content
|
// Only send final events if we have actually output content
|
||||||
if (*param).(*Params).HasContent {
|
if (*param).(*Params).HasContent {
|
||||||
// Close the final content block
|
// Close the final content block
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
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: `
|
|
||||||
|
|
||||||
// Create the message delta template with appropriate stop reason
|
// 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
|
// Set tool_use stop reason if tools were used in this response
|
||||||
if usedTool {
|
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" {
|
} 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
|
// Include thinking tokens in output token count if present
|
||||||
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
|
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
|
||||||
template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
|
template, _ = sjson.SetBytes(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
|
||||||
template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int())
|
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.
|
// 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.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Claude-compatible JSON response.
|
// - []byte: A Claude-compatible JSON response.
|
||||||
func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
_ = originalRequestRawJSON
|
_ = originalRequestRawJSON
|
||||||
_ = requestRawJSON
|
_ = requestRawJSON
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
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 := []byte(`{"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.SetBytes(out, "id", root.Get("response.responseId").String())
|
||||||
out, _ = sjson.Set(out, "model", root.Get("response.modelVersion").String())
|
out, _ = sjson.SetBytes(out, "model", root.Get("response.modelVersion").String())
|
||||||
|
|
||||||
inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
|
inputTokens := root.Get("response.usageMetadata.promptTokenCount").Int()
|
||||||
outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int()
|
outputTokens := root.Get("response.usageMetadata.candidatesTokenCount").Int() + root.Get("response.usageMetadata.thoughtsTokenCount").Int()
|
||||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||||
|
|
||||||
parts := root.Get("response.candidates.0.content.parts")
|
parts := root.Get("response.candidates.0.content.parts")
|
||||||
textBuilder := strings.Builder{}
|
textBuilder := strings.Builder{}
|
||||||
@@ -297,9 +272,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", textBuilder.String())
|
block, _ = sjson.SetBytes(block, "text", textBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,9 +282,9 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,15 +308,15 @@ func ConvertGeminiCLIResponseToClaudeNonStream(_ context.Context, _ string, orig
|
|||||||
|
|
||||||
name := functionCall.Get("name").String()
|
name := functionCall.Get("name").String()
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
toolBlock, _ = sjson.SetBytes(toolBlock, "id", fmt.Sprintf("tool_%d", toolIDCounter))
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "name", name)
|
toolBlock, _ = sjson.SetBytes(toolBlock, "name", name)
|
||||||
inputRaw := "{}"
|
inputRaw := "{}"
|
||||||
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
||||||
inputRaw = args.Raw
|
inputRaw = args.Raw
|
||||||
}
|
}
|
||||||
toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
|
toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw))
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
|
out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock)
|
||||||
continue
|
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() {
|
if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("response.usageMetadata").Exists() {
|
||||||
out, _ = sjson.Delete(out, "usage")
|
out, _ = sjson.DeleteBytes(out, "usage")
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,23 +34,23 @@ import (
|
|||||||
// - []byte: The transformed request data in Gemini API format
|
// - []byte: The transformed request data in Gemini API format
|
||||||
func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []byte {
|
func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
template := ""
|
template := []byte(`{"project":"","request":{},"model":""}`)
|
||||||
template = `{"project":"","request":{},"model":""}`
|
template, _ = sjson.SetRawBytes(template, "request", rawJSON)
|
||||||
template, _ = sjson.SetRaw(template, "request", string(rawJSON))
|
template, _ = sjson.SetBytes(template, "model", gjson.GetBytes(template, "request.model").String())
|
||||||
template, _ = sjson.Set(template, "model", gjson.Get(template, "request.model").String())
|
template, _ = sjson.DeleteBytes(template, "request.model")
|
||||||
template, _ = sjson.Delete(template, "request.model")
|
|
||||||
|
|
||||||
template, errFixCLIToolResponse := fixCLIToolResponse(template)
|
templateStr, errFixCLIToolResponse := fixCLIToolResponse(string(template))
|
||||||
if errFixCLIToolResponse != nil {
|
if errFixCLIToolResponse != nil {
|
||||||
return []byte{}
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
template = []byte(templateStr)
|
||||||
|
|
||||||
systemInstructionResult := gjson.Get(template, "request.system_instruction")
|
systemInstructionResult := gjson.GetBytes(template, "request.system_instruction")
|
||||||
if systemInstructionResult.Exists() {
|
if systemInstructionResult.Exists() {
|
||||||
template, _ = sjson.SetRaw(template, "request.systemInstruction", systemInstructionResult.Raw)
|
template, _ = sjson.SetRawBytes(template, "request.systemInstruction", []byte(systemInstructionResult.Raw))
|
||||||
template, _ = sjson.Delete(template, "request.system_instruction")
|
template, _ = sjson.DeleteBytes(template, "request.system_instruction")
|
||||||
}
|
}
|
||||||
rawJSON = []byte(template)
|
rawJSON = template
|
||||||
|
|
||||||
// Normalize roles in request.contents: default to valid values if missing/invalid
|
// Normalize roles in request.contents: default to valid values if missing/invalid
|
||||||
contents := gjson.GetBytes(rawJSON, "request.contents")
|
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:
|
// Filter out contents with empty parts to avoid Gemini API error:
|
||||||
// "required oneof field 'data' must have one initialized field"
|
// "required oneof field 'data' must have one initialized field"
|
||||||
filteredContents := "[]"
|
filteredContents := []byte(`[]`)
|
||||||
hasFiltered := false
|
hasFiltered := false
|
||||||
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
|
gjson.GetBytes(rawJSON, "request.contents").ForEach(func(_, content gjson.Result) bool {
|
||||||
parts := content.Get("parts")
|
parts := content.Get("parts")
|
||||||
@@ -121,11 +121,11 @@ func ConvertGeminiRequestToGeminiCLI(_ string, inputRawJSON []byte, _ bool) []by
|
|||||||
hasFiltered = true
|
hasFiltered = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
filteredContents, _ = sjson.SetRaw(filteredContents, "-1", content.Raw)
|
filteredContents, _ = sjson.SetRawBytes(filteredContents, "-1", []byte(content.Raw))
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if hasFiltered {
|
if hasFiltered {
|
||||||
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", []byte(filteredContents))
|
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents", filteredContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
return common.AttachDefaultSafetySettings(rawJSON, "request.safetySettings")
|
return common.AttachDefaultSafetySettings(rawJSON, "request.safetySettings")
|
||||||
@@ -142,7 +142,8 @@ type FunctionCallGroup struct {
|
|||||||
func backfillFunctionResponseName(raw string, fallbackName string) string {
|
func backfillFunctionResponseName(raw string, fallbackName string) string {
|
||||||
name := gjson.Get(raw, "functionResponse.name").String()
|
name := gjson.Get(raw, "functionResponse.name").String()
|
||||||
if strings.TrimSpace(name) == "" && fallbackName != "" {
|
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
|
return raw
|
||||||
}
|
}
|
||||||
@@ -171,7 +172,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize data structures for processing and grouping
|
// Initialize data structures for processing and grouping
|
||||||
contentsWrapper := `{"contents":[]}`
|
contentsWrapper := []byte(`{"contents":[]}`)
|
||||||
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
var pendingGroups []*FunctionCallGroup // Groups awaiting completion with responses
|
||||||
var collectedResponses []gjson.Result // Standalone responses to be matched
|
var collectedResponses []gjson.Result // Standalone responses to be matched
|
||||||
|
|
||||||
@@ -204,18 +205,18 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
// Create merged function response content
|
// Create merged function response content
|
||||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
functionResponseContent := []byte(`{"parts":[],"role":"function"}`)
|
||||||
for ri, response := range groupResponses {
|
for ri, response := range groupResponses {
|
||||||
if !response.IsObject() {
|
if !response.IsObject() {
|
||||||
log.Warnf("failed to parse function response")
|
log.Warnf("failed to parse function response")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raw := backfillFunctionResponseName(response.Raw, group.CallNames[ri])
|
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 {
|
if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +239,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
log.Warnf("failed to parse model content")
|
log.Warnf("failed to parse model content")
|
||||||
return true
|
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
|
// Create a new group for tracking responses
|
||||||
group := &FunctionCallGroup{
|
group := &FunctionCallGroup{
|
||||||
@@ -252,7 +253,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
log.Warnf("failed to parse content")
|
log.Warnf("failed to parse content")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Non-model content (user, etc.)
|
// Non-model content (user, etc.)
|
||||||
@@ -260,7 +261,7 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
log.Warnf("failed to parse content")
|
log.Warnf("failed to parse content")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", value.Raw)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", []byte(value.Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -272,25 +273,25 @@ func fixCLIToolResponse(input string) (string, error) {
|
|||||||
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
groupResponses := collectedResponses[:group.ResponsesNeeded]
|
||||||
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
collectedResponses = collectedResponses[group.ResponsesNeeded:]
|
||||||
|
|
||||||
functionResponseContent := `{"parts":[],"role":"function"}`
|
functionResponseContent := []byte(`{"parts":[],"role":"function"}`)
|
||||||
for ri, response := range groupResponses {
|
for ri, response := range groupResponses {
|
||||||
if !response.IsObject() {
|
if !response.IsObject() {
|
||||||
log.Warnf("failed to parse function response")
|
log.Warnf("failed to parse function response")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
raw := backfillFunctionResponseName(response.Raw, group.CallNames[ri])
|
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 {
|
if gjson.GetBytes(functionResponseContent, "parts.#").Int() > 0 {
|
||||||
contentsWrapper, _ = sjson.SetRaw(contentsWrapper, "contents.-1", functionResponseContent)
|
contentsWrapper, _ = sjson.SetRawBytes(contentsWrapper, "contents.-1", functionResponseContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the original JSON with the new contents
|
// Update the original JSON with the new contents
|
||||||
result := input
|
result := []byte(input)
|
||||||
result, _ = sjson.SetRaw(result, "request.contents", gjson.Get(contentsWrapper, "contents").Raw)
|
result, _ = sjson.SetRawBytes(result, "request.contents", []byte(gjson.GetBytes(contentsWrapper, "contents").Raw))
|
||||||
|
|
||||||
return result, nil
|
return string(result), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -29,8 +29,8 @@ import (
|
|||||||
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: The transformed request data in Gemini API format
|
// - [][]byte: The transformed request data in Gemini API format
|
||||||
func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string {
|
func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte {
|
||||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
}
|
}
|
||||||
@@ -43,22 +43,22 @@ func ConvertGeminiCliResponseToGemini(ctx context.Context, _ string, originalReq
|
|||||||
chunk = []byte(responseResult.Raw)
|
chunk = []byte(responseResult.Raw)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
chunkTemplate := "[]"
|
chunkTemplate := []byte(`[]`)
|
||||||
responseResult := gjson.ParseBytes(chunk)
|
responseResult := gjson.ParseBytes(chunk)
|
||||||
if responseResult.IsArray() {
|
if responseResult.IsArray() {
|
||||||
responseResultItems := responseResult.Array()
|
responseResultItems := responseResult.Array()
|
||||||
for i := 0; i < len(responseResultItems); i++ {
|
for i := 0; i < len(responseResultItems); i++ {
|
||||||
responseResultItem := responseResultItems[i]
|
responseResultItem := responseResultItems[i]
|
||||||
if responseResultItem.Get("response").Exists() {
|
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.
|
// 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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response containing the response data
|
// - []byte: A Gemini-compatible JSON response containing the response data
|
||||||
func ConvertGeminiCliResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertGeminiCliResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
responseResult := gjson.GetBytes(rawJSON, "response")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
return responseResult.Raw
|
return []byte(responseResult.Raw)
|
||||||
}
|
}
|
||||||
return string(rawJSON)
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,43 +299,43 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
|
|||||||
if t.Get("type").String() == "function" {
|
if t.Get("type").String() == "function" {
|
||||||
fn := t.Get("function")
|
fn := t.Get("function")
|
||||||
if fn.Exists() && fn.IsObject() {
|
if fn.Exists() && fn.IsObject() {
|
||||||
fnRaw := fn.Raw
|
fnRaw := []byte(fn.Raw)
|
||||||
if fn.Get("parameters").Exists() {
|
if fn.Get("parameters").Exists() {
|
||||||
renamed, errRename := util.RenameKey(fnRaw, "parameters", "parametersJsonSchema")
|
renamed, errRename := util.RenameKey(fn.Raw, "parameters", "parametersJsonSchema")
|
||||||
if errRename != nil {
|
if errRename != nil {
|
||||||
log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename)
|
log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename)
|
||||||
var errSet error
|
var errSet error
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object")
|
fnRaw, errSet = sjson.SetBytes(fnRaw, "parametersJsonSchema.type", "object")
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
fnRaw, errSet = sjson.SetRawBytes(fnRaw, "parametersJsonSchema.properties", []byte(`{}`))
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fnRaw = renamed
|
fnRaw = []byte(renamed)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var errSet error
|
var errSet error
|
||||||
fnRaw, errSet = sjson.Set(fnRaw, "parametersJsonSchema.type", "object")
|
fnRaw, errSet = sjson.SetBytes(fnRaw, "parametersJsonSchema.type", "object")
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
fnRaw, errSet = sjson.SetRawBytes(fnRaw, "parametersJsonSchema.properties", []byte(`{}`))
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
fnRaw, _ = sjson.DeleteBytes(fnRaw, "strict")
|
||||||
if !hasFunction {
|
if !hasFunction {
|
||||||
functionToolNode, _ = sjson.SetRawBytes(functionToolNode, "functionDeclarations", []byte("[]"))
|
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 {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to append tool declaration for '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to append tool declaration for '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ var functionCallIDCounter uint64
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
// - [][]byte: A slice of OpenAI-compatible JSON responses
|
||||||
func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &convertCliResponseToOpenAIChatParams{
|
*param = &convertCliResponseToOpenAIChatParams{
|
||||||
UnixTimestamp: 0,
|
UnixTimestamp: 0,
|
||||||
@@ -51,15 +51,15 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the OpenAI SSE template.
|
// 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.
|
// Extract and set the model version.
|
||||||
if modelVersionResult := gjson.GetBytes(rawJSON, "response.modelVersion"); modelVersionResult.Exists() {
|
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.
|
// Extract and set the creation timestamp.
|
||||||
@@ -68,14 +68,14 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp = t.Unix()
|
(*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp = t.Unix()
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", (*param).(*convertCliResponseToOpenAIChatParams).UnixTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set the response ID.
|
// Extract and set the response ID.
|
||||||
if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() {
|
if responseIDResult := gjson.GetBytes(rawJSON, "response.responseId"); responseIDResult.Exists() {
|
||||||
template, _ = sjson.Set(template, "id", responseIDResult.String())
|
template, _ = sjson.SetBytes(template, "id", responseIDResult.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
finishReason := ""
|
finishReason := ""
|
||||||
@@ -93,21 +93,21 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() {
|
if usageResult := gjson.GetBytes(rawJSON, "response.usageMetadata"); usageResult.Exists() {
|
||||||
cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
|
cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
|
||||||
if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.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() {
|
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()
|
promptTokenCount := usageResult.Get("promptTokenCount").Int()
|
||||||
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").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 {
|
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)
|
// Include cached token count if present (indicates prompt caching is working)
|
||||||
if cachedTokenCount > 0 {
|
if cachedTokenCount > 0 {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Warnf("gemini-cli openai response: failed to set cached_tokens: %v", err)
|
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.
|
// Handle text content, distinguishing between regular content and reasoning/thoughts.
|
||||||
if partResult.Get("thought").Bool() {
|
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 {
|
} 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() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function call content.
|
// Handle function call content.
|
||||||
hasFunctionCall = true
|
hasFunctionCall = true
|
||||||
toolCallsResult := gjson.Get(template, "choices.0.delta.tool_calls")
|
toolCallsResult := gjson.GetBytes(template, "choices.0.delta.tool_calls")
|
||||||
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
functionCallIndex := (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex
|
||||||
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
(*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex++
|
||||||
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
||||||
functionCallIndex = len(toolCallsResult.Array())
|
functionCallIndex = len(toolCallsResult.Array())
|
||||||
} else {
|
} 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()
|
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.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
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.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
||||||
} else if inlineDataResult.Exists() {
|
} else if inlineDataResult.Exists() {
|
||||||
data := inlineDataResult.Get("data").String()
|
data := inlineDataResult.Get("data").String()
|
||||||
if data == "" {
|
if data == "" {
|
||||||
@@ -185,32 +185,32 @@ func ConvertCliResponseToOpenAI(_ context.Context, _ string, originalRequestRawJ
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
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() {
|
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())
|
imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array())
|
||||||
imagePayload := `{"type":"image_url","image_url":{"url":""}}`
|
imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL)
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasFunctionCall {
|
if hasFunctionCall {
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", "tool_calls")
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
|
template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", "tool_calls")
|
||||||
} else if finishReason != "" && (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex == 0 {
|
} else if finishReason != "" && (*param).(*convertCliResponseToOpenAIChatParams).FunctionIndex == 0 {
|
||||||
// Only pass through specific finish reasons
|
// Only pass through specific finish reasons
|
||||||
if finishReason == "max_tokens" || finishReason == "stop" {
|
if finishReason == "max_tokens" || finishReason == "stop" {
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason)
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason)
|
||||||
template, _ = sjson.Set(template, "choices.0.native_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.
|
// 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
|
// - param: A pointer to a parameter object for the conversion
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []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) string {
|
func ConvertCliResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||||
responseResult := gjson.GetBytes(rawJSON, "response")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
return ConvertGeminiResponseToOpenAINonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, []byte(responseResult.Raw), param)
|
return ConvertGeminiResponseToOpenAINonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, []byte(responseResult.Raw), param)
|
||||||
}
|
}
|
||||||
return ""
|
return []byte{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"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")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
rawJSON = []byte(responseResult.Raw)
|
rawJSON = []byte(responseResult.Raw)
|
||||||
@@ -15,7 +15,7 @@ func ConvertGeminiCLIResponseToOpenAIResponses(ctx context.Context, modelName st
|
|||||||
return ConvertGeminiResponseToOpenAIResponses(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
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")
|
responseResult := gjson.GetBytes(rawJSON, "response")
|
||||||
if responseResult.Exists() {
|
if responseResult.Exists() {
|
||||||
rawJSON = []byte(responseResult.Raw)
|
rawJSON = []byte(responseResult.Raw)
|
||||||
|
|||||||
@@ -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)
|
rawJSON = bytes.Replace(rawJSON, []byte(`"url":{"type":"string","format":"uri",`), []byte(`"url":{"type":"string",`), -1)
|
||||||
|
|
||||||
// Build output Gemini CLI request JSON
|
// Build output Gemini CLI request JSON
|
||||||
out := `{"contents":[]}`
|
out := []byte(`{"contents":[]}`)
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// system instruction
|
// system instruction
|
||||||
if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
|
if systemResult := gjson.GetBytes(rawJSON, "system"); systemResult.IsArray() {
|
||||||
systemInstruction := `{"role":"user","parts":[]}`
|
systemInstruction := []byte(`{"role":"user","parts":[]}`)
|
||||||
hasSystemParts := false
|
hasSystemParts := false
|
||||||
systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
|
systemResult.ForEach(func(_, systemPromptResult gjson.Result) bool {
|
||||||
if systemPromptResult.Get("type").String() == "text" {
|
if systemPromptResult.Get("type").String() == "text" {
|
||||||
textResult := systemPromptResult.Get("text")
|
textResult := systemPromptResult.Get("text")
|
||||||
if textResult.Type == gjson.String {
|
if textResult.Type == gjson.String {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", textResult.String())
|
part, _ = sjson.SetBytes(part, "text", textResult.String())
|
||||||
systemInstruction, _ = sjson.SetRaw(systemInstruction, "parts.-1", part)
|
systemInstruction, _ = sjson.SetRawBytes(systemInstruction, "parts.-1", part)
|
||||||
hasSystemParts = true
|
hasSystemParts = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if hasSystemParts {
|
if hasSystemParts {
|
||||||
out, _ = sjson.SetRaw(out, "system_instruction", systemInstruction)
|
out, _ = sjson.SetRawBytes(out, "system_instruction", systemInstruction)
|
||||||
}
|
}
|
||||||
} else if systemResult.Type == gjson.String {
|
} 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
|
// contents
|
||||||
@@ -71,17 +71,17 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
role = "model"
|
role = "model"
|
||||||
}
|
}
|
||||||
|
|
||||||
contentJSON := `{"role":"","parts":[]}`
|
contentJSON := []byte(`{"role":"","parts":[]}`)
|
||||||
contentJSON, _ = sjson.Set(contentJSON, "role", role)
|
contentJSON, _ = sjson.SetBytes(contentJSON, "role", role)
|
||||||
|
|
||||||
contentsResult := messageResult.Get("content")
|
contentsResult := messageResult.Get("content")
|
||||||
if contentsResult.IsArray() {
|
if contentsResult.IsArray() {
|
||||||
contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
|
contentsResult.ForEach(func(_, contentResult gjson.Result) bool {
|
||||||
switch contentResult.Get("type").String() {
|
switch contentResult.Get("type").String() {
|
||||||
case "text":
|
case "text":
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", contentResult.Get("text").String())
|
part, _ = sjson.SetBytes(part, "text", contentResult.Get("text").String())
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
|
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
functionName := contentResult.Get("name").String()
|
functionName := contentResult.Get("name").String()
|
||||||
@@ -93,11 +93,11 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
functionArgs := contentResult.Get("input").String()
|
functionArgs := contentResult.Get("input").String()
|
||||||
argsResult := gjson.Parse(functionArgs)
|
argsResult := gjson.Parse(functionArgs)
|
||||||
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
if argsResult.IsObject() && gjson.Valid(functionArgs) {
|
||||||
part := `{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`
|
part := []byte(`{"thoughtSignature":"","functionCall":{"name":"","args":{}}}`)
|
||||||
part, _ = sjson.Set(part, "thoughtSignature", geminiClaudeThoughtSignature)
|
part, _ = sjson.SetBytes(part, "thoughtSignature", geminiClaudeThoughtSignature)
|
||||||
part, _ = sjson.Set(part, "functionCall.name", functionName)
|
part, _ = sjson.SetBytes(part, "functionCall.name", functionName)
|
||||||
part, _ = sjson.SetRaw(part, "functionCall.args", functionArgs)
|
part, _ = sjson.SetRawBytes(part, "functionCall.args", []byte(functionArgs))
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tool_result":
|
case "tool_result":
|
||||||
@@ -110,10 +110,10 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
funcName = toolCallID
|
funcName = toolCallID
|
||||||
}
|
}
|
||||||
responseData := contentResult.Get("content").Raw
|
responseData := contentResult.Get("content").Raw
|
||||||
part := `{"functionResponse":{"name":"","response":{"result":""}}}`
|
part := []byte(`{"functionResponse":{"name":"","response":{"result":""}}}`)
|
||||||
part, _ = sjson.Set(part, "functionResponse.name", funcName)
|
part, _ = sjson.SetBytes(part, "functionResponse.name", funcName)
|
||||||
part, _ = sjson.Set(part, "functionResponse.response.result", responseData)
|
part, _ = sjson.SetBytes(part, "functionResponse.response.result", responseData)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
|
|
||||||
case "image":
|
case "image":
|
||||||
source := contentResult.Get("source")
|
source := contentResult.Get("source")
|
||||||
@@ -125,19 +125,19 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if mimeType == "" || data == "" {
|
if mimeType == "" || data == "" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
part := `{"inline_data":{"mime_type":"","data":""}}`
|
part := []byte(`{"inline_data":{"mime_type":"","data":""}}`)
|
||||||
part, _ = sjson.Set(part, "inline_data.mime_type", mimeType)
|
part, _ = sjson.SetBytes(part, "inline_data.mime_type", mimeType)
|
||||||
part, _ = sjson.Set(part, "inline_data.data", data)
|
part, _ = sjson.SetBytes(part, "inline_data.data", data)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", contentJSON)
|
||||||
} else if contentsResult.Type == gjson.String {
|
} else if contentsResult.Type == gjson.String {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", contentsResult.String())
|
part, _ = sjson.SetBytes(part, "text", contentsResult.String())
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "parts.-1", part)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "parts.-1", part)
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", contentJSON)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", contentJSON)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -150,25 +150,33 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
inputSchemaResult := toolResult.Get("input_schema")
|
inputSchemaResult := toolResult.Get("input_schema")
|
||||||
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
if inputSchemaResult.Exists() && inputSchemaResult.IsObject() {
|
||||||
inputSchema := inputSchemaResult.Raw
|
inputSchema := inputSchemaResult.Raw
|
||||||
tool, _ := sjson.Delete(toolResult.Raw, "input_schema")
|
tool := []byte(toolResult.Raw)
|
||||||
tool, _ = sjson.SetRaw(tool, "parametersJsonSchema", inputSchema)
|
var err error
|
||||||
tool, _ = sjson.Delete(tool, "strict")
|
tool, err = sjson.DeleteBytes(tool, "input_schema")
|
||||||
tool, _ = sjson.Delete(tool, "input_examples")
|
if err != nil {
|
||||||
tool, _ = sjson.Delete(tool, "type")
|
return true
|
||||||
tool, _ = sjson.Delete(tool, "cache_control")
|
}
|
||||||
tool, _ = sjson.Delete(tool, "defer_loading")
|
tool, err = sjson.SetRawBytes(tool, "parametersJsonSchema", []byte(inputSchema))
|
||||||
if gjson.Valid(tool) && gjson.Parse(tool).IsObject() {
|
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 {
|
if !hasTools {
|
||||||
out, _ = sjson.SetRaw(out, "tools", `[{"functionDeclarations":[]}]`)
|
out, _ = sjson.SetRawBytes(out, "tools", []byte(`[{"functionDeclarations":[]}]`))
|
||||||
hasTools = true
|
hasTools = true
|
||||||
}
|
}
|
||||||
out, _ = sjson.SetRaw(out, "tools.0.functionDeclarations.-1", tool)
|
out, _ = sjson.SetRawBytes(out, "tools.0.functionDeclarations.-1", tool)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if !hasTools {
|
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 {
|
switch toolChoiceType {
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "AUTO")
|
||||||
case "none":
|
case "none":
|
||||||
out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "NONE")
|
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "NONE")
|
||||||
case "any":
|
case "any":
|
||||||
out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
case "tool":
|
case "tool":
|
||||||
out, _ = sjson.Set(out, "toolConfig.functionCallingConfig.mode", "ANY")
|
out, _ = sjson.SetBytes(out, "toolConfig.functionCallingConfig.mode", "ANY")
|
||||||
if toolChoiceName != "" {
|
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":
|
case "enabled":
|
||||||
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
if b := t.Get("budget_tokens"); b.Exists() && b.Type == gjson.Number {
|
||||||
budget := int(b.Int())
|
budget := int(b.Int())
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingBudget", budget)
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.includeThoughts", true)
|
out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.includeThoughts", true)
|
||||||
}
|
}
|
||||||
case "adaptive", "auto":
|
case "adaptive", "auto":
|
||||||
// For adaptive thinking:
|
// For adaptive thinking:
|
||||||
@@ -219,32 +227,32 @@ func ConvertClaudeRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingLevel", effort)
|
out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingLevel", effort)
|
||||||
} else {
|
} else {
|
||||||
maxBudget := 0
|
maxBudget := 0
|
||||||
if mi := registry.LookupModelInfo(modelName, "gemini"); mi != nil && mi.Thinking != nil {
|
if mi := registry.LookupModelInfo(modelName, "gemini"); mi != nil && mi.Thinking != nil {
|
||||||
maxBudget = mi.Thinking.Max
|
maxBudget = mi.Thinking.Max
|
||||||
}
|
}
|
||||||
if maxBudget > 0 {
|
if maxBudget > 0 {
|
||||||
out, _ = sjson.Set(out, "generationConfig.thinkingConfig.thinkingBudget", maxBudget)
|
out, _ = sjson.SetBytes(out, "generationConfig.thinkingConfig.thinkingBudget", maxBudget)
|
||||||
} else {
|
} 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 {
|
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 {
|
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 {
|
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")
|
result = common.AttachDefaultSafetySettings(result, "safetySettings")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -47,8 +48,8 @@ var toolUseIDCounter uint64
|
|||||||
// - param: A pointer to a parameter object for the conversion.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Claude-compatible JSON response.
|
// - [][]byte: A slice of bytes, each containing a Claude-compatible SSE payload.
|
||||||
func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &Params{
|
*param = &Params{
|
||||||
IsGlAPIKey: false,
|
IsGlAPIKey: false,
|
||||||
@@ -63,32 +64,31 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
// Only send message_stop if we have actually output content
|
// Only send message_stop if we have actually output content
|
||||||
if (*param).(*Params).HasContent {
|
if (*param).(*Params).HasContent {
|
||||||
return []string{
|
return [][]byte{translatorcommon.AppendSSEEventString(nil, "message_stop", `{"type":"message_stop"}`, 3)}
|
||||||
"event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n\n",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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
|
// Initialize the streaming session with a message_start event
|
||||||
// This is only sent for the very first response chunk
|
// This is only sent for the very first response chunk
|
||||||
if !(*param).(*Params).HasFirstResponse {
|
if !(*param).(*Params).HasFirstResponse {
|
||||||
output = "event: message_start\n"
|
|
||||||
|
|
||||||
// Create the initial message structure with default values
|
// Create the initial message structure with default values
|
||||||
// This follows the Claude API specification for streaming message initialization
|
// 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
|
// Override default values with actual response metadata if available
|
||||||
if modelVersionResult := gjson.GetBytes(rawJSON, "modelVersion"); modelVersionResult.Exists() {
|
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() {
|
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
|
(*param).(*Params).HasFirstResponse = true
|
||||||
}
|
}
|
||||||
@@ -111,9 +111,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
if partResult.Get("thought").Bool() {
|
if partResult.Get("thought").Bool() {
|
||||||
// Continue existing thinking block
|
// Continue existing thinking block
|
||||||
if (*param).(*Params).ResponseType == 2 {
|
if (*param).(*Params).ResponseType == 2 {
|
||||||
output = output + "event: content_block_delta\n"
|
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, (*param).(*Params).ResponseIndex)), "delta.thinking", partTextResult.String())
|
||||||
data, _ := sjson.Set(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))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
} else {
|
} else {
|
||||||
// Transition from another state to thinking
|
// 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 + 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 + "\n\n\n"
|
||||||
}
|
}
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new thinking content block
|
// Start a new thinking content block
|
||||||
output = output + "event: content_block_start\n"
|
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"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())
|
||||||
output = output + "\n\n\n"
|
appendEvent("content_block_delta", string(data))
|
||||||
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)
|
|
||||||
(*param).(*Params).ResponseType = 2 // Set state to thinking
|
(*param).(*Params).ResponseType = 2 // Set state to thinking
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
}
|
}
|
||||||
@@ -144,9 +138,8 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
// Process regular text content (user-visible output)
|
// Process regular text content (user-visible output)
|
||||||
// Continue existing text block
|
// Continue existing text block
|
||||||
if (*param).(*Params).ResponseType == 1 {
|
if (*param).(*Params).ResponseType == 1 {
|
||||||
output = output + "event: content_block_delta\n"
|
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"text_delta","text":""}}`, (*param).(*Params).ResponseIndex)), "delta.text", partTextResult.String())
|
||||||
data, _ := sjson.Set(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))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
} else {
|
} else {
|
||||||
// Transition from another state to text content
|
// 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 + 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 + "\n\n\n"
|
||||||
}
|
}
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new text content block
|
// Start a new text content block
|
||||||
output = output + "event: content_block_start\n"
|
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"text","text":""}}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"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())
|
||||||
output = output + "\n\n\n"
|
appendEvent("content_block_delta", string(data))
|
||||||
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)
|
|
||||||
(*param).(*Params).ResponseType = 1 // Set state to content
|
(*param).(*Params).ResponseType = 1 // Set state to content
|
||||||
(*param).(*Params).HasContent = true
|
(*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 we are already in tool use mode and name is empty, treat as continuation (delta).
|
||||||
if (*param).(*Params).ResponseType == 3 && upstreamToolName == "" {
|
if (*param).(*Params).ResponseType == 3 && upstreamToolName == "" {
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
output = output + "event: content_block_delta\n"
|
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)
|
||||||
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)
|
appendEvent("content_block_delta", string(data))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
}
|
}
|
||||||
// Continue to next part without closing/opening logic
|
// Continue to next part without closing/opening logic
|
||||||
continue
|
continue
|
||||||
@@ -196,9 +183,7 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
// Handle state transitions when switching to function calls
|
// Handle state transitions when switching to function calls
|
||||||
// Close any existing function call block first
|
// Close any existing function call block first
|
||||||
if (*param).(*Params).ResponseType == 3 {
|
if (*param).(*Params).ResponseType == 3 {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
(*param).(*Params).ResponseType = 0
|
(*param).(*Params).ResponseType = 0
|
||||||
}
|
}
|
||||||
@@ -212,26 +197,21 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
|
|
||||||
// Close any other existing content block
|
// Close any other existing content block
|
||||||
if (*param).(*Params).ResponseType != 0 {
|
if (*param).(*Params).ResponseType != 0 {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
(*param).(*Params).ResponseIndex++
|
(*param).(*Params).ResponseIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new tool use content block
|
// Start a new tool use content block
|
||||||
// This creates the structure for a function call in Claude format
|
// 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
|
// 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 := []byte(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.SetBytes(data, "content_block.id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, atomic.AddUint64(&toolUseIDCounter, 1))))
|
||||||
data, _ = sjson.Set(data, "content_block.name", clientToolName)
|
data, _ = sjson.SetBytes(data, "content_block.name", clientToolName)
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
appendEvent("content_block_start", string(data))
|
||||||
|
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
||||||
output = output + "event: content_block_delta\n"
|
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)
|
||||||
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)
|
appendEvent("content_block_delta", string(data))
|
||||||
output = output + fmt.Sprintf("data: %s\n\n\n", data)
|
|
||||||
}
|
}
|
||||||
(*param).(*Params).ResponseType = 3
|
(*param).(*Params).ResponseType = 3
|
||||||
(*param).(*Params).HasContent = true
|
(*param).(*Params).HasContent = true
|
||||||
@@ -244,30 +224,25 @@ func ConvertGeminiResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() {
|
if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() {
|
||||||
// Only send final events if we have actually output content
|
// Only send final events if we have actually output content
|
||||||
if (*param).(*Params).HasContent {
|
if (*param).(*Params).HasContent {
|
||||||
output = output + "event: content_block_stop\n"
|
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex))
|
||||||
output = output + fmt.Sprintf(`data: {"type":"content_block_stop","index":%d}`, (*param).(*Params).ResponseIndex)
|
|
||||||
output = output + "\n\n\n"
|
|
||||||
|
|
||||||
output = output + "event: message_delta\n"
|
template := []byte(`{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`)
|
||||||
output = output + `data: `
|
|
||||||
|
|
||||||
template := `{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
|
||||||
if (*param).(*Params).SawToolCall {
|
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" {
|
} 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()
|
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
|
||||||
template, _ = sjson.Set(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
|
template, _ = sjson.SetBytes(template, "usage.output_tokens", candidatesTokenCountResult.Int()+thoughtsTokenCount)
|
||||||
template, _ = sjson.Set(template, "usage.input_tokens", usageResult.Get("promptTokenCount").Int())
|
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.
|
// 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.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Claude-compatible JSON response.
|
// - []byte: A Claude-compatible JSON response.
|
||||||
func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
_ = requestRawJSON
|
_ = requestRawJSON
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
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 := []byte(`{"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.SetBytes(out, "id", root.Get("responseId").String())
|
||||||
out, _ = sjson.Set(out, "model", root.Get("modelVersion").String())
|
out, _ = sjson.SetBytes(out, "model", root.Get("modelVersion").String())
|
||||||
|
|
||||||
inputTokens := root.Get("usageMetadata.promptTokenCount").Int()
|
inputTokens := root.Get("usageMetadata.promptTokenCount").Int()
|
||||||
outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int()
|
outputTokens := root.Get("usageMetadata.candidatesTokenCount").Int() + root.Get("usageMetadata.thoughtsTokenCount").Int()
|
||||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||||
|
|
||||||
parts := root.Get("candidates.0.content.parts")
|
parts := root.Get("candidates.0.content.parts")
|
||||||
textBuilder := strings.Builder{}
|
textBuilder := strings.Builder{}
|
||||||
@@ -305,9 +280,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", textBuilder.String())
|
block, _ = sjson.SetBytes(block, "text", textBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,9 +290,9 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,15 +317,15 @@ func ConvertGeminiResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
upstreamToolName := functionCall.Get("name").String()
|
upstreamToolName := functionCall.Get("name").String()
|
||||||
clientToolName := util.MapToolName(toolNameMap, upstreamToolName)
|
clientToolName := util.MapToolName(toolNameMap, upstreamToolName)
|
||||||
toolIDCounter++
|
toolIDCounter++
|
||||||
toolBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, toolIDCounter)))
|
toolBlock, _ = sjson.SetBytes(toolBlock, "id", util.SanitizeClaudeToolID(fmt.Sprintf("%s-%d", upstreamToolName, toolIDCounter)))
|
||||||
toolBlock, _ = sjson.Set(toolBlock, "name", clientToolName)
|
toolBlock, _ = sjson.SetBytes(toolBlock, "name", clientToolName)
|
||||||
inputRaw := "{}"
|
inputRaw := "{}"
|
||||||
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
if args := functionCall.Get("args"); args.Exists() && gjson.Valid(args.Raw) && args.IsObject() {
|
||||||
inputRaw = args.Raw
|
inputRaw = args.Raw
|
||||||
}
|
}
|
||||||
toolBlock, _ = sjson.SetRaw(toolBlock, "input", inputRaw)
|
toolBlock, _ = sjson.SetRawBytes(toolBlock, "input", []byte(inputRaw))
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", toolBlock)
|
out, _ = sjson.SetRawBytes(out, "content.-1", toolBlock)
|
||||||
continue
|
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() {
|
if inputTokens == int64(0) && outputTokens == int64(0) && !root.Get("usageMetadata").Exists() {
|
||||||
out, _ = sjson.Delete(out, "usage")
|
out, _ = sjson.DeleteBytes(out, "usage")
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ package geminiCLI
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,19 +26,18 @@ var dataTag = []byte("data:")
|
|||||||
// - param: A pointer to a parameter object for the conversion (unused).
|
// - param: A pointer to a parameter object for the conversion (unused).
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini CLI-compatible JSON response.
|
// - [][]byte: A slice of Gemini CLI-compatible JSON responses.
|
||||||
func ConvertGeminiResponseToGeminiCLI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []string {
|
func ConvertGeminiResponseToGeminiCLI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) [][]byte {
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
json := `{"response": {}}`
|
rawJSON, _ = sjson.SetRawBytes([]byte(`{"response":{}}`), "response", rawJSON)
|
||||||
rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON)
|
return [][]byte{rawJSON}
|
||||||
return []string{string(rawJSON)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertGeminiResponseToGeminiCLINonStream converts a non-streaming Gemini response to a non-streaming Gemini CLI response.
|
// 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).
|
// - param: A pointer to a parameter object for the conversion (unused).
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini CLI-compatible JSON response.
|
// - []byte: A Gemini CLI-compatible JSON response.
|
||||||
func ConvertGeminiResponseToGeminiCLINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertGeminiResponseToGeminiCLINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
json := `{"response": {}}`
|
rawJSON, _ = sjson.SetRawBytes([]byte(`{"response":{}}`), "response", rawJSON)
|
||||||
rawJSON, _ = sjson.SetRawBytes([]byte(json), "response", rawJSON)
|
return rawJSON
|
||||||
return string(rawJSON)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiCLITokenCount(ctx context.Context, count int64) string {
|
func GeminiCLITokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,28 @@ package gemini
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PassthroughGeminiResponseStream forwards Gemini responses unchanged.
|
// 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:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []string{string(rawJSON)}
|
return [][]byte{rawJSON}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassthroughGeminiResponseNonStream forwards Gemini responses unchanged.
|
// PassthroughGeminiResponseNonStream forwards Gemini responses unchanged.
|
||||||
func PassthroughGeminiResponseNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func PassthroughGeminiResponseNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
return string(rawJSON)
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,31 +311,35 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
|
|||||||
if errRename != nil {
|
if errRename != nil {
|
||||||
log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename)
|
log.Warnf("Failed to rename parameters for tool '%s': %v", fn.Get("name").String(), errRename)
|
||||||
var errSet error
|
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 {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
fnRawBytes, errSet = sjson.SetRawBytes(fnRawBytes, "parametersJsonSchema.properties", []byte(`{}`))
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fnRaw = string(fnRawBytes)
|
||||||
} else {
|
} else {
|
||||||
fnRaw = renamed
|
fnRaw = renamed
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var errSet error
|
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 {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema type for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fnRaw, errSet = sjson.SetRaw(fnRaw, "parametersJsonSchema.properties", `{}`)
|
fnRawBytes, errSet = sjson.SetRawBytes(fnRawBytes, "parametersJsonSchema.properties", []byte(`{}`))
|
||||||
if errSet != nil {
|
if errSet != nil {
|
||||||
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
log.Warnf("Failed to set default schema properties for tool '%s': %v", fn.Get("name").String(), errSet)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
fnRaw = string(fnRawBytes)
|
||||||
}
|
}
|
||||||
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
fnRaw, _ = sjson.Delete(fnRaw, "strict")
|
||||||
if !hasFunction {
|
if !hasFunction {
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ var functionCallIDCounter uint64
|
|||||||
// - param: A pointer to a parameter object for maintaining state between calls
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
// - [][]byte: A slice of OpenAI-compatible JSON responses
|
||||||
func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
// Initialize parameters if nil.
|
// Initialize parameters if nil.
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &convertGeminiResponseToOpenAIChatParams{
|
*param = &convertGeminiResponseToOpenAIChatParams{
|
||||||
@@ -62,16 +62,16 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the OpenAI SSE base template.
|
// Initialize the OpenAI SSE base template.
|
||||||
// We use a base template and clone it for each candidate to support multiple candidates.
|
// 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.
|
// Extract and set the model version.
|
||||||
if modelVersionResult := gjson.GetBytes(rawJSON, "modelVersion"); modelVersionResult.Exists() {
|
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.
|
// Extract and set the creation timestamp.
|
||||||
@@ -80,14 +80,14 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
p.UnixTimestamp = t.Unix()
|
p.UnixTimestamp = t.Unix()
|
||||||
}
|
}
|
||||||
baseTemplate, _ = sjson.Set(baseTemplate, "created", p.UnixTimestamp)
|
baseTemplate, _ = sjson.SetBytes(baseTemplate, "created", p.UnixTimestamp)
|
||||||
} else {
|
} else {
|
||||||
baseTemplate, _ = sjson.Set(baseTemplate, "created", p.UnixTimestamp)
|
baseTemplate, _ = sjson.SetBytes(baseTemplate, "created", p.UnixTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract and set the response ID.
|
// Extract and set the response ID.
|
||||||
if responseIDResult := gjson.GetBytes(rawJSON, "responseId"); responseIDResult.Exists() {
|
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).
|
// 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() {
|
if usageResult := gjson.GetBytes(rawJSON, "usageMetadata"); usageResult.Exists() {
|
||||||
cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
|
cachedTokenCount := usageResult.Get("cachedContentTokenCount").Int()
|
||||||
if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.Exists() {
|
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() {
|
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()
|
promptTokenCount := usageResult.Get("promptTokenCount").Int()
|
||||||
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").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 {
|
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)
|
// Include cached token count if present (indicates prompt caching is working)
|
||||||
if cachedTokenCount > 0 {
|
if cachedTokenCount > 0 {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Warnf("gemini openai response: failed to set cached_tokens in streaming: %v", err)
|
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")
|
candidates := gjson.GetBytes(rawJSON, "candidates")
|
||||||
|
|
||||||
// Iterate over all candidates to support candidate_count > 1.
|
// Iterate over all candidates to support candidate_count > 1.
|
||||||
if candidates.IsArray() {
|
if candidates.IsArray() {
|
||||||
candidates.ForEach(func(_, candidate gjson.Result) bool {
|
candidates.ForEach(func(_, candidate gjson.Result) bool {
|
||||||
// Clone the template for the current candidate.
|
// Clone the template for the current candidate.
|
||||||
template := baseTemplate
|
template := append([]byte(nil), baseTemplate...)
|
||||||
|
|
||||||
// Set the specific index for this candidate.
|
// Set the specific index for this candidate.
|
||||||
candidateIndex := int(candidate.Get("index").Int())
|
candidateIndex := int(candidate.Get("index").Int())
|
||||||
template, _ = sjson.Set(template, "choices.0.index", candidateIndex)
|
template, _ = sjson.SetBytes(template, "choices.0.index", candidateIndex)
|
||||||
|
|
||||||
finishReason := ""
|
finishReason := ""
|
||||||
if stopReasonResult := gjson.GetBytes(rawJSON, "stop_reason"); stopReasonResult.Exists() {
|
if stopReasonResult := gjson.GetBytes(rawJSON, "stop_reason"); stopReasonResult.Exists() {
|
||||||
@@ -170,15 +170,15 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
text := partTextResult.String()
|
text := partTextResult.String()
|
||||||
// Handle text content, distinguishing between regular content and reasoning/thoughts.
|
// Handle text content, distinguishing between regular content and reasoning/thoughts.
|
||||||
if partResult.Get("thought").Bool() {
|
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 {
|
} 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() {
|
} else if functionCallResult.Exists() {
|
||||||
// Handle function call content.
|
// Handle function call content.
|
||||||
hasFunctionCall = true
|
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.
|
// Retrieve the function index for this specific candidate.
|
||||||
functionCallIndex := p.FunctionIndex[candidateIndex]
|
functionCallIndex := p.FunctionIndex[candidateIndex]
|
||||||
@@ -187,19 +187,19 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
if toolCallsResult.Exists() && toolCallsResult.IsArray() {
|
||||||
functionCallIndex = len(toolCallsResult.Array())
|
functionCallIndex = len(toolCallsResult.Array())
|
||||||
} else {
|
} 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()
|
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.SetBytes(functionCallTemplate, "id", fmt.Sprintf("%s-%d-%d", fcName, time.Now().UnixNano(), atomic.AddUint64(&functionCallIDCounter, 1)))
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "index", functionCallIndex)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "index", functionCallIndex)
|
||||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "function.name", fcName)
|
functionCallTemplate, _ = sjson.SetBytes(functionCallTemplate, "function.name", fcName)
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
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.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.tool_calls.-1", functionCallTemplate)
|
||||||
} else if inlineDataResult.Exists() {
|
} else if inlineDataResult.Exists() {
|
||||||
data := inlineDataResult.Get("data").String()
|
data := inlineDataResult.Get("data").String()
|
||||||
if data == "" {
|
if data == "" {
|
||||||
@@ -213,28 +213,28 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
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() {
|
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())
|
imageIndex := len(gjson.GetBytes(template, "choices.0.delta.images").Array())
|
||||||
imagePayload := `{"type":"image_url","image_url":{"url":""}}`
|
imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL)
|
||||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
template, _ = sjson.SetBytes(template, "choices.0.delta.role", "assistant")
|
||||||
template, _ = sjson.SetRaw(template, "choices.0.delta.images.-1", imagePayload)
|
template, _ = sjson.SetRawBytes(template, "choices.0.delta.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasFunctionCall {
|
if hasFunctionCall {
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", "tool_calls")
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", "tool_calls")
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", "tool_calls")
|
template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", "tool_calls")
|
||||||
} else if finishReason != "" {
|
} else if finishReason != "" {
|
||||||
// Only pass through specific finish reasons
|
// Only pass through specific finish reasons
|
||||||
if finishReason == "max_tokens" || finishReason == "stop" {
|
if finishReason == "max_tokens" || finishReason == "stop" {
|
||||||
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReason)
|
template, _ = sjson.SetBytes(template, "choices.0.finish_reason", finishReason)
|
||||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReason)
|
template, _ = sjson.SetBytes(template, "choices.0.native_finish_reason", finishReason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ func ConvertGeminiResponseToOpenAI(_ context.Context, _ string, originalRequestR
|
|||||||
} else {
|
} else {
|
||||||
// If there are no candidates (e.g., a pure usageMetadata chunk), return the usage chunk if present.
|
// 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 {
|
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)
|
// - param: A pointer to a parameter object for the conversion (unused in current implementation)
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []byte: An OpenAI-compatible JSON response containing all message content and metadata
|
||||||
func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
var unixTimestamp int64
|
var unixTimestamp int64
|
||||||
// Initialize template with an empty choices array to support multiple candidates.
|
// 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() {
|
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() {
|
if createTimeResult := gjson.GetBytes(rawJSON, "createTime"); createTimeResult.Exists() {
|
||||||
@@ -278,33 +278,33 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
unixTimestamp = t.Unix()
|
unixTimestamp = t.Unix()
|
||||||
}
|
}
|
||||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", unixTimestamp)
|
||||||
} else {
|
} else {
|
||||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
template, _ = sjson.SetBytes(template, "created", unixTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if responseIDResult := gjson.GetBytes(rawJSON, "responseId"); responseIDResult.Exists() {
|
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 usageResult := gjson.GetBytes(rawJSON, "usageMetadata"); usageResult.Exists() {
|
||||||
if candidatesTokenCountResult := usageResult.Get("candidatesTokenCount"); candidatesTokenCountResult.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() {
|
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()
|
promptTokenCount := usageResult.Get("promptTokenCount").Int()
|
||||||
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
|
thoughtsTokenCount := usageResult.Get("thoughtsTokenCount").Int()
|
||||||
cachedTokenCount := usageResult.Get("cachedContentTokenCount").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 {
|
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)
|
// Include cached token count if present (indicates prompt caching is working)
|
||||||
if cachedTokenCount > 0 {
|
if cachedTokenCount > 0 {
|
||||||
var err error
|
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 {
|
if err != nil {
|
||||||
log.Warnf("gemini openai response: failed to set cached_tokens in non-streaming: %v", err)
|
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() {
|
if candidates.IsArray() {
|
||||||
candidates.ForEach(func(_, candidate gjson.Result) bool {
|
candidates.ForEach(func(_, candidate gjson.Result) bool {
|
||||||
// Construct a single Choice object.
|
// 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.
|
// 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.
|
// Set finish reason.
|
||||||
if finishReasonResult := candidate.Get("finishReason"); finishReasonResult.Exists() {
|
if finishReasonResult := candidate.Get("finishReason"); finishReasonResult.Exists() {
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "finish_reason", strings.ToLower(finishReasonResult.String()))
|
choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "finish_reason", strings.ToLower(finishReasonResult.String()))
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "native_finish_reason", strings.ToLower(finishReasonResult.String()))
|
choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "native_finish_reason", strings.ToLower(finishReasonResult.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
partsResult := candidate.Get("content.parts")
|
partsResult := candidate.Get("content.parts")
|
||||||
@@ -343,29 +343,29 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
if partTextResult.Exists() {
|
if partTextResult.Exists() {
|
||||||
// Append text content, distinguishing between regular content and reasoning.
|
// Append text content, distinguishing between regular content and reasoning.
|
||||||
if partResult.Get("thought").Bool() {
|
if partResult.Get("thought").Bool() {
|
||||||
oldVal := gjson.Get(choiceTemplate, "message.reasoning_content").String()
|
oldVal := gjson.GetBytes(choiceTemplate, "message.reasoning_content").String()
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "message.reasoning_content", oldVal+partTextResult.String())
|
choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.reasoning_content", oldVal+partTextResult.String())
|
||||||
} else {
|
} else {
|
||||||
oldVal := gjson.Get(choiceTemplate, "message.content").String()
|
oldVal := gjson.GetBytes(choiceTemplate, "message.content").String()
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "message.content", oldVal+partTextResult.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() {
|
} else if functionCallResult.Exists() {
|
||||||
// Append function call content to the tool_calls array.
|
// Append function call content to the tool_calls array.
|
||||||
hasFunctionCall = true
|
hasFunctionCall = true
|
||||||
toolCallsResult := gjson.Get(choiceTemplate, "message.tool_calls")
|
toolCallsResult := gjson.GetBytes(choiceTemplate, "message.tool_calls")
|
||||||
if !toolCallsResult.Exists() || !toolCallsResult.IsArray() {
|
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()
|
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.SetBytes(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, "function.name", fcName)
|
||||||
if fcArgsResult := functionCallResult.Get("args"); fcArgsResult.Exists() {
|
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.SetBytes(choiceTemplate, "message.role", "assistant")
|
||||||
choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.tool_calls.-1", functionCallItemTemplate)
|
choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.tool_calls.-1", functionCallItemTemplate)
|
||||||
} else if inlineDataResult.Exists() {
|
} else if inlineDataResult.Exists() {
|
||||||
data := inlineDataResult.Get("data").String()
|
data := inlineDataResult.Get("data").String()
|
||||||
if data != "" {
|
if data != "" {
|
||||||
@@ -377,28 +377,28 @@ func ConvertGeminiResponseToOpenAINonStream(_ context.Context, _ string, origina
|
|||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
}
|
}
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
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() {
|
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())
|
imageIndex := len(gjson.GetBytes(choiceTemplate, "message.images").Array())
|
||||||
imagePayload := `{"type":"image_url","image_url":{"url":""}}`
|
imagePayload := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "index", imageIndex)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "index", imageIndex)
|
||||||
imagePayload, _ = sjson.Set(imagePayload, "image_url.url", imageURL)
|
imagePayload, _ = sjson.SetBytes(imagePayload, "image_url.url", imageURL)
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "message.role", "assistant")
|
choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "message.role", "assistant")
|
||||||
choiceTemplate, _ = sjson.SetRaw(choiceTemplate, "message.images.-1", imagePayload)
|
choiceTemplate, _ = sjson.SetRawBytes(choiceTemplate, "message.images.-1", imagePayload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasFunctionCall {
|
if hasFunctionCall {
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "finish_reason", "tool_calls")
|
choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "finish_reason", "tool_calls")
|
||||||
choiceTemplate, _ = sjson.Set(choiceTemplate, "native_finish_reason", "tool_calls")
|
choiceTemplate, _ = sjson.SetBytes(choiceTemplate, "native_finish_reason", "tool_calls")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the constructed choice to the main choices array.
|
// 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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
_ = stream // Unused but required by interface
|
_ = stream // Unused but required by interface
|
||||||
|
|
||||||
// Base Gemini API template (do not include thinkingConfig by default)
|
// Base Gemini API template (do not include thinkingConfig by default)
|
||||||
out := `{"contents":[]}`
|
out := []byte(`{"contents":[]}`)
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Extract system instruction from OpenAI "instructions" field
|
// Extract system instruction from OpenAI "instructions" field
|
||||||
if instructions := root.Get("instructions"); instructions.Exists() {
|
if instructions := root.Get("instructions"); instructions.Exists() {
|
||||||
systemInstr := `{"parts":[{"text":""}]}`
|
systemInstr := []byte(`{"parts":[{"text":""}]}`)
|
||||||
systemInstr, _ = sjson.Set(systemInstr, "parts.0.text", instructions.String())
|
systemInstr, _ = sjson.SetBytes(systemInstr, "parts.0.text", instructions.String())
|
||||||
out, _ = sjson.SetRaw(out, "systemInstruction", systemInstr)
|
out, _ = sjson.SetRawBytes(out, "systemInstruction", systemInstr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert input messages to Gemini contents format
|
// Convert input messages to Gemini contents format
|
||||||
@@ -78,8 +78,8 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
|
|
||||||
if len(calls) > 0 {
|
if len(calls) > 0 {
|
||||||
outputMap := make(map[string]gjson.Result, len(outputs))
|
outputMap := make(map[string]gjson.Result, len(outputs))
|
||||||
for _, out := range outputs {
|
for _, outItem := range outputs {
|
||||||
outputMap[out.Get("call_id").String()] = out
|
outputMap[outItem.Get("call_id").String()] = outItem
|
||||||
}
|
}
|
||||||
for _, call := range calls {
|
for _, call := range calls {
|
||||||
normalized = append(normalized, call)
|
normalized = append(normalized, call)
|
||||||
@@ -89,9 +89,9 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
delete(outputMap, callID)
|
delete(outputMap, callID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, out := range outputs {
|
for _, outItem := range outputs {
|
||||||
if _, ok := outputMap[out.Get("call_id").String()]; ok {
|
if _, ok := outputMap[outItem.Get("call_id").String()]; ok {
|
||||||
normalized = append(normalized, out)
|
normalized = append(normalized, outItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -119,29 +119,27 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
case "message":
|
case "message":
|
||||||
if strings.EqualFold(itemRole, "system") {
|
if strings.EqualFold(itemRole, "system") {
|
||||||
if contentArray := item.Get("content"); contentArray.Exists() {
|
if contentArray := item.Get("content"); contentArray.Exists() {
|
||||||
systemInstr := ""
|
systemInstr := []byte(`{"parts":[]}`)
|
||||||
if systemInstructionResult := gjson.Get(out, "systemInstruction"); systemInstructionResult.Exists() {
|
if systemInstructionResult := gjson.GetBytes(out, "systemInstruction"); systemInstructionResult.Exists() {
|
||||||
systemInstr = systemInstructionResult.Raw
|
systemInstr = []byte(systemInstructionResult.Raw)
|
||||||
} else {
|
|
||||||
systemInstr = `{"parts":[]}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if contentArray.IsArray() {
|
if contentArray.IsArray() {
|
||||||
contentArray.ForEach(func(_, contentItem gjson.Result) bool {
|
contentArray.ForEach(func(_, contentItem gjson.Result) bool {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
text := contentItem.Get("text").String()
|
text := contentItem.Get("text").String()
|
||||||
part, _ = sjson.Set(part, "text", text)
|
part, _ = sjson.SetBytes(part, "text", text)
|
||||||
systemInstr, _ = sjson.SetRaw(systemInstr, "parts.-1", part)
|
systemInstr, _ = sjson.SetRawBytes(systemInstr, "parts.-1", part)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else if contentArray.Type == gjson.String {
|
} else if contentArray.Type == gjson.String {
|
||||||
part := `{"text":""}`
|
part := []byte(`{"text":""}`)
|
||||||
part, _ = sjson.Set(part, "text", contentArray.String())
|
part, _ = sjson.SetBytes(part, "text", contentArray.String())
|
||||||
systemInstr, _ = sjson.SetRaw(systemInstr, "parts.-1", part)
|
systemInstr, _ = sjson.SetRawBytes(systemInstr, "parts.-1", part)
|
||||||
}
|
}
|
||||||
|
|
||||||
if systemInstr != `{"parts":[]}` {
|
if gjson.GetBytes(systemInstr, "parts.#").Int() > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "systemInstruction", systemInstr)
|
out, _ = sjson.SetRawBytes(out, "systemInstruction", systemInstr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -153,20 +151,20 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
// with roles derived from the content type to match docs/convert-2.md.
|
// with roles derived from the content type to match docs/convert-2.md.
|
||||||
if contentArray := item.Get("content"); contentArray.Exists() && contentArray.IsArray() {
|
if contentArray := item.Get("content"); contentArray.Exists() && contentArray.IsArray() {
|
||||||
currentRole := ""
|
currentRole := ""
|
||||||
var currentParts []string
|
currentParts := make([][]byte, 0)
|
||||||
|
|
||||||
flush := func() {
|
flush := func() {
|
||||||
if currentRole == "" || len(currentParts) == 0 {
|
if currentRole == "" || len(currentParts) == 0 {
|
||||||
currentParts = nil
|
currentParts = currentParts[:0]
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
one := `{"role":"","parts":[]}`
|
one := []byte(`{"role":"","parts":[]}`)
|
||||||
one, _ = sjson.Set(one, "role", currentRole)
|
one, _ = sjson.SetBytes(one, "role", currentRole)
|
||||||
for _, part := range currentParts {
|
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)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", one)
|
||||||
currentParts = nil
|
currentParts = currentParts[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
contentArray.ForEach(func(_, contentItem gjson.Result) bool {
|
contentArray.ForEach(func(_, contentItem gjson.Result) bool {
|
||||||
@@ -199,12 +197,12 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
currentRole = effRole
|
currentRole = effRole
|
||||||
}
|
}
|
||||||
|
|
||||||
var partJSON string
|
var partJSON []byte
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case "input_text", "output_text":
|
case "input_text", "output_text":
|
||||||
if text := contentItem.Get("text"); text.Exists() {
|
if text := contentItem.Get("text"); text.Exists() {
|
||||||
partJSON = `{"text":""}`
|
partJSON = []byte(`{"text":""}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "text", text.String())
|
partJSON, _ = sjson.SetBytes(partJSON, "text", text.String())
|
||||||
}
|
}
|
||||||
case "input_image":
|
case "input_image":
|
||||||
imageURL := contentItem.Get("image_url").String()
|
imageURL := contentItem.Get("image_url").String()
|
||||||
@@ -233,9 +231,9 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if data != "" {
|
if data != "" {
|
||||||
partJSON = `{"inline_data":{"mime_type":"","data":""}}`
|
partJSON = []byte(`{"inline_data":{"mime_type":"","data":""}}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "inline_data.mime_type", mimeType)
|
partJSON, _ = sjson.SetBytes(partJSON, "inline_data.mime_type", mimeType)
|
||||||
partJSON, _ = sjson.Set(partJSON, "inline_data.data", data)
|
partJSON, _ = sjson.SetBytes(partJSON, "inline_data.data", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "input_audio":
|
case "input_audio":
|
||||||
@@ -261,13 +259,13 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
mimeType = "audio/" + audioFormat
|
mimeType = "audio/" + audioFormat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
partJSON = `{"inline_data":{"mime_type":"","data":""}}`
|
partJSON = []byte(`{"inline_data":{"mime_type":"","data":""}}`)
|
||||||
partJSON, _ = sjson.Set(partJSON, "inline_data.mime_type", mimeType)
|
partJSON, _ = sjson.SetBytes(partJSON, "inline_data.mime_type", mimeType)
|
||||||
partJSON, _ = sjson.Set(partJSON, "inline_data.data", audioData)
|
partJSON, _ = sjson.SetBytes(partJSON, "inline_data.data", audioData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if partJSON != "" {
|
if len(partJSON) > 0 {
|
||||||
currentParts = append(currentParts, partJSON)
|
currentParts = append(currentParts, partJSON)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -285,30 +283,31 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
one := `{"role":"","parts":[{"text":""}]}`
|
one := []byte(`{"role":"","parts":[{"text":""}]}`)
|
||||||
one, _ = sjson.Set(one, "role", effRole)
|
one, _ = sjson.SetBytes(one, "role", effRole)
|
||||||
one, _ = sjson.Set(one, "parts.0.text", contentArray.String())
|
one, _ = sjson.SetBytes(one, "parts.0.text", contentArray.String())
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", one)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", one)
|
||||||
}
|
}
|
||||||
|
|
||||||
case "function_call":
|
case "function_call":
|
||||||
// Handle function calls - convert to model message with functionCall
|
// Handle function calls - convert to model message with functionCall
|
||||||
name := item.Get("name").String()
|
name := item.Get("name").String()
|
||||||
arguments := item.Get("arguments").String()
|
arguments := item.Get("arguments").String()
|
||||||
|
|
||||||
modelContent := `{"role":"model","parts":[]}`
|
modelContent := []byte(`{"role":"model","parts":[]}`)
|
||||||
functionCall := `{"functionCall":{"name":"","args":{}}}`
|
functionCall := []byte(`{"functionCall":{"name":"","args":{}}}`)
|
||||||
functionCall, _ = sjson.Set(functionCall, "functionCall.name", name)
|
functionCall, _ = sjson.SetBytes(functionCall, "functionCall.name", name)
|
||||||
functionCall, _ = sjson.Set(functionCall, "thoughtSignature", geminiResponsesThoughtSignature)
|
functionCall, _ = sjson.SetBytes(functionCall, "thoughtSignature", geminiResponsesThoughtSignature)
|
||||||
functionCall, _ = sjson.Set(functionCall, "functionCall.id", item.Get("call_id").String())
|
functionCall, _ = sjson.SetBytes(functionCall, "functionCall.id", item.Get("call_id").String())
|
||||||
|
|
||||||
// Parse arguments JSON string and set as args object
|
// Parse arguments JSON string and set as args object
|
||||||
if arguments != "" {
|
if arguments != "" {
|
||||||
argsResult := gjson.Parse(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)
|
modelContent, _ = sjson.SetRawBytes(modelContent, "parts.-1", functionCall)
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", modelContent)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", modelContent)
|
||||||
|
|
||||||
case "function_call_output":
|
case "function_call_output":
|
||||||
// Handle function call outputs - convert to function message with functionResponse
|
// 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)
|
// Use .Raw to preserve the JSON encoding (includes quotes for strings)
|
||||||
outputRaw := item.Get("output").Str
|
outputRaw := item.Get("output").Str
|
||||||
|
|
||||||
functionContent := `{"role":"function","parts":[]}`
|
functionContent := []byte(`{"role":"function","parts":[]}`)
|
||||||
functionResponse := `{"functionResponse":{"name":"","response":{}}}`
|
functionResponse := []byte(`{"functionResponse":{"name":"","response":{}}}`)
|
||||||
|
|
||||||
// We need to extract the function name from the previous function_call
|
// 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
|
// 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.SetBytes(functionResponse, "functionResponse.name", functionName)
|
||||||
functionResponse, _ = sjson.Set(functionResponse, "functionResponse.id", callID)
|
functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.id", callID)
|
||||||
|
|
||||||
// Set the raw JSON output directly (preserves string encoding)
|
// Set the raw JSON output directly (preserves string encoding)
|
||||||
if outputRaw != "" && outputRaw != "null" {
|
if outputRaw != "" && outputRaw != "null" {
|
||||||
output := gjson.Parse(outputRaw)
|
output := gjson.Parse(outputRaw)
|
||||||
if output.Type == gjson.JSON && json.Valid([]byte(output.Raw)) {
|
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 {
|
} else {
|
||||||
functionResponse, _ = sjson.Set(functionResponse, "functionResponse.response.result", outputRaw)
|
functionResponse, _ = sjson.SetBytes(functionResponse, "functionResponse.response.result", outputRaw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
functionContent, _ = sjson.SetRaw(functionContent, "parts.-1", functionResponse)
|
functionContent, _ = sjson.SetRawBytes(functionContent, "parts.-1", functionResponse)
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", functionContent)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", functionContent)
|
||||||
|
|
||||||
case "reasoning":
|
case "reasoning":
|
||||||
thoughtContent := `{"role":"model","parts":[]}`
|
thoughtContent := []byte(`{"role":"model","parts":[]}`)
|
||||||
thought := `{"text":"","thoughtSignature":"","thought":true}`
|
thought := []byte(`{"text":"","thoughtSignature":"","thought":true}`)
|
||||||
thought, _ = sjson.Set(thought, "text", item.Get("summary.0.text").String())
|
thought, _ = sjson.SetBytes(thought, "text", item.Get("summary.0.text").String())
|
||||||
thought, _ = sjson.Set(thought, "thoughtSignature", item.Get("encrypted_content").String())
|
thought, _ = sjson.SetBytes(thought, "thoughtSignature", item.Get("encrypted_content").String())
|
||||||
|
|
||||||
thoughtContent, _ = sjson.SetRaw(thoughtContent, "parts.-1", thought)
|
thoughtContent, _ = sjson.SetRawBytes(thoughtContent, "parts.-1", thought)
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", thoughtContent)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", thoughtContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if input.Exists() && input.Type == gjson.String {
|
} else if input.Exists() && input.Type == gjson.String {
|
||||||
// Simple string input conversion to user message
|
// Simple string input conversion to user message
|
||||||
userContent := `{"role":"user","parts":[{"text":""}]}`
|
userContent := []byte(`{"role":"user","parts":[{"text":""}]}`)
|
||||||
userContent, _ = sjson.Set(userContent, "parts.0.text", input.String())
|
userContent, _ = sjson.SetBytes(userContent, "parts.0.text", input.String())
|
||||||
out, _ = sjson.SetRaw(out, "contents.-1", userContent)
|
out, _ = sjson.SetRawBytes(out, "contents.-1", userContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert tools to Gemini functionDeclarations format
|
// Convert tools to Gemini functionDeclarations format
|
||||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||||
geminiTools := `[{"functionDeclarations":[]}]`
|
geminiTools := []byte(`[{"functionDeclarations":[]}]`)
|
||||||
|
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if tool.Get("type").String() == "function" {
|
if tool.Get("type").String() == "function" {
|
||||||
funcDecl := `{"name":"","description":"","parametersJsonSchema":{}}`
|
funcDecl := []byte(`{"name":"","description":"","parametersJsonSchema":{}}`)
|
||||||
|
|
||||||
if name := tool.Get("name"); name.Exists() {
|
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() {
|
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() {
|
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
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Only add tools if there are function declarations
|
// Only add tools if there are function declarations
|
||||||
if funcDecls := gjson.Get(geminiTools, "0.functionDeclarations"); funcDecls.Exists() && len(funcDecls.Array()) > 0 {
|
if funcDecls := gjson.GetBytes(geminiTools, "0.functionDeclarations"); funcDecls.Exists() && len(funcDecls.Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "tools", geminiTools)
|
out, _ = sjson.SetRawBytes(out, "tools", geminiTools)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle generation config from OpenAI format
|
// Handle generation config from OpenAI format
|
||||||
if maxOutputTokens := root.Get("max_output_tokens"); maxOutputTokens.Exists() {
|
if maxOutputTokens := root.Get("max_output_tokens"); maxOutputTokens.Exists() {
|
||||||
genConfig := `{"maxOutputTokens":0}`
|
genConfig := []byte(`{"maxOutputTokens":0}`)
|
||||||
genConfig, _ = sjson.Set(genConfig, "maxOutputTokens", maxOutputTokens.Int())
|
genConfig, _ = sjson.SetBytes(genConfig, "maxOutputTokens", maxOutputTokens.Int())
|
||||||
out, _ = sjson.SetRaw(out, "generationConfig", genConfig)
|
out, _ = sjson.SetRawBytes(out, "generationConfig", genConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle temperature if present
|
// Handle temperature if present
|
||||||
if temperature := root.Get("temperature"); temperature.Exists() {
|
if temperature := root.Get("temperature"); temperature.Exists() {
|
||||||
if !gjson.Get(out, "generationConfig").Exists() {
|
if !gjson.GetBytes(out, "generationConfig").Exists() {
|
||||||
out, _ = sjson.SetRaw(out, "generationConfig", `{}`)
|
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
|
// Handle top_p if present
|
||||||
if topP := root.Get("top_p"); topP.Exists() {
|
if topP := root.Get("top_p"); topP.Exists() {
|
||||||
if !gjson.Get(out, "generationConfig").Exists() {
|
if !gjson.GetBytes(out, "generationConfig").Exists() {
|
||||||
out, _ = sjson.SetRaw(out, "generationConfig", `{}`)
|
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
|
// Handle stop sequences
|
||||||
if stopSequences := root.Get("stop_sequences"); stopSequences.Exists() && stopSequences.IsArray() {
|
if stopSequences := root.Get("stop_sequences"); stopSequences.Exists() && stopSequences.IsArray() {
|
||||||
if !gjson.Get(out, "generationConfig").Exists() {
|
if !gjson.GetBytes(out, "generationConfig").Exists() {
|
||||||
out, _ = sjson.SetRaw(out, "generationConfig", `{}`)
|
out, _ = sjson.SetRawBytes(out, "generationConfig", []byte(`{}`))
|
||||||
}
|
}
|
||||||
var sequences []string
|
var sequences []string
|
||||||
stopSequences.ForEach(func(_, seq gjson.Result) bool {
|
stopSequences.ForEach(func(_, seq gjson.Result) bool {
|
||||||
sequences = append(sequences, seq.String())
|
sequences = append(sequences, seq.String())
|
||||||
return true
|
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.
|
// Apply thinking configuration: convert OpenAI Responses API reasoning.effort to Gemini thinkingConfig.
|
||||||
@@ -440,16 +439,16 @@ func ConvertOpenAIResponsesRequestToGemini(modelName string, inputRawJSON []byte
|
|||||||
if effort != "" {
|
if effort != "" {
|
||||||
thinkingPath := "generationConfig.thinkingConfig"
|
thinkingPath := "generationConfig.thinkingConfig"
|
||||||
if effort == "auto" {
|
if effort == "auto" {
|
||||||
out, _ = sjson.Set(out, thinkingPath+".thinkingBudget", -1)
|
out, _ = sjson.SetBytes(out, thinkingPath+".thinkingBudget", -1)
|
||||||
out, _ = sjson.Set(out, thinkingPath+".includeThoughts", true)
|
out, _ = sjson.SetBytes(out, thinkingPath+".includeThoughts", true)
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, thinkingPath+".thinkingLevel", effort)
|
out, _ = sjson.SetBytes(out, thinkingPath+".thinkingLevel", effort)
|
||||||
out, _ = sjson.Set(out, thinkingPath+".includeThoughts", effort != "none")
|
out, _ = sjson.SetBytes(out, thinkingPath+".includeThoughts", effort != "none")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := []byte(out)
|
result := out
|
||||||
result = common.AttachDefaultSafetySettings(result, "safetySettings")
|
result = common.AttachDefaultSafetySettings(result, "safetySettings")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -81,12 +82,12 @@ func unwrapGeminiResponseRoot(root gjson.Result) gjson.Result {
|
|||||||
return root
|
return root
|
||||||
}
|
}
|
||||||
|
|
||||||
func emitEvent(event string, payload string) string {
|
func emitEvent(event string, payload []byte) []byte {
|
||||||
return fmt.Sprintf("event: %s\ndata: %s", event, payload)
|
return translatorcommon.SSEEventData(event, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertGeminiResponseToOpenAIResponses converts Gemini SSE chunks into OpenAI Responses SSE events.
|
// 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 {
|
if *param == nil {
|
||||||
*param = &geminiToResponsesState{
|
*param = &geminiToResponsesState{
|
||||||
FuncArgsBuf: make(map[int]*strings.Builder),
|
FuncArgsBuf: make(map[int]*strings.Builder),
|
||||||
@@ -115,16 +116,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
|
|
||||||
rawJSON = bytes.TrimSpace(rawJSON)
|
rawJSON = bytes.TrimSpace(rawJSON)
|
||||||
if len(rawJSON) == 0 || bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if len(rawJSON) == 0 || bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
if !root.Exists() {
|
if !root.Exists() {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
root = unwrapGeminiResponseRoot(root)
|
root = unwrapGeminiResponseRoot(root)
|
||||||
|
|
||||||
var out []string
|
var out [][]byte
|
||||||
nextSeq := func() int { st.Seq++; return st.Seq }
|
nextSeq := func() int { st.Seq++; return st.Seq }
|
||||||
|
|
||||||
// Helper to finalize reasoning summary events in correct order.
|
// Helper to finalize reasoning summary events in correct order.
|
||||||
@@ -135,26 +136,26 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
full := st.ReasoningBuf.String()
|
full := st.ReasoningBuf.String()
|
||||||
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
textDone := []byte(`{"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.SetBytes(textDone, "sequence_number", nextSeq())
|
||||||
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningItemID)
|
textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningItemID)
|
||||||
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex)
|
||||||
textDone, _ = sjson.Set(textDone, "text", full)
|
textDone, _ = sjson.SetBytes(textDone, "text", full)
|
||||||
out = append(out, emitEvent("response.reasoning_summary_text.done", textDone))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningItemID)
|
partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningItemID)
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex)
|
||||||
partDone, _ = sjson.Set(partDone, "part.text", full)
|
partDone, _ = sjson.SetBytes(partDone, "part.text", full)
|
||||||
out = append(out, emitEvent("response.reasoning_summary_part.done", partDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", st.ReasoningItemID)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", st.ReasoningItemID)
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", st.ReasoningIndex)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", st.ReasoningIndex)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.encrypted_content", st.ReasoningEnc)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.encrypted_content", st.ReasoningEnc)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.summary.0.text", full)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.summary.0.text", full)
|
||||||
out = append(out, emitEvent("response.output_item.done", itemDone))
|
out = append(out, emitEvent("response.output_item.done", itemDone))
|
||||||
|
|
||||||
st.ReasoningClosed = true
|
st.ReasoningClosed = true
|
||||||
@@ -168,23 +169,23 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fullText := st.ItemTextBuf.String()
|
fullText := st.ItemTextBuf.String()
|
||||||
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
done := []byte(`{"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.SetBytes(done, "sequence_number", nextSeq())
|
||||||
done, _ = sjson.Set(done, "item_id", st.CurrentMsgID)
|
done, _ = sjson.SetBytes(done, "item_id", st.CurrentMsgID)
|
||||||
done, _ = sjson.Set(done, "output_index", st.MsgIndex)
|
done, _ = sjson.SetBytes(done, "output_index", st.MsgIndex)
|
||||||
done, _ = sjson.Set(done, "text", fullText)
|
done, _ = sjson.SetBytes(done, "text", fullText)
|
||||||
out = append(out, emitEvent("response.output_text.done", done))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", st.CurrentMsgID)
|
partDone, _ = sjson.SetBytes(partDone, "item_id", st.CurrentMsgID)
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", st.MsgIndex)
|
partDone, _ = sjson.SetBytes(partDone, "output_index", st.MsgIndex)
|
||||||
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
|
||||||
out = append(out, emitEvent("response.content_part.done", partDone))
|
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 := []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.Set(final, "sequence_number", nextSeq())
|
final, _ = sjson.SetBytes(final, "sequence_number", nextSeq())
|
||||||
final, _ = sjson.Set(final, "output_index", st.MsgIndex)
|
final, _ = sjson.SetBytes(final, "output_index", st.MsgIndex)
|
||||||
final, _ = sjson.Set(final, "item.id", st.CurrentMsgID)
|
final, _ = sjson.SetBytes(final, "item.id", st.CurrentMsgID)
|
||||||
final, _ = sjson.Set(final, "item.content.0.text", fullText)
|
final, _ = sjson.SetBytes(final, "item.content.0.text", fullText)
|
||||||
out = append(out, emitEvent("response.output_item.done", final))
|
out = append(out, emitEvent("response.output_item.done", final))
|
||||||
|
|
||||||
st.MsgClosed = true
|
st.MsgClosed = true
|
||||||
@@ -208,16 +209,16 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
st.CreatedAt = time.Now().Unix()
|
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 := []byte(`{"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.SetBytes(created, "sequence_number", nextSeq())
|
||||||
created, _ = sjson.Set(created, "response.id", st.ResponseID)
|
created, _ = sjson.SetBytes(created, "response.id", st.ResponseID)
|
||||||
created, _ = sjson.Set(created, "response.created_at", st.CreatedAt)
|
created, _ = sjson.SetBytes(created, "response.created_at", st.CreatedAt)
|
||||||
out = append(out, emitEvent("response.created", created))
|
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 := []byte(`{"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.SetBytes(inprog, "sequence_number", nextSeq())
|
||||||
inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID)
|
inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID)
|
||||||
inprog, _ = sjson.Set(inprog, "response.created_at", st.CreatedAt)
|
inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.CreatedAt)
|
||||||
out = append(out, emitEvent("response.in_progress", inprog))
|
out = append(out, emitEvent("response.in_progress", inprog))
|
||||||
|
|
||||||
st.Started = true
|
st.Started = true
|
||||||
@@ -243,25 +244,25 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
st.ReasoningIndex = st.NextIndex
|
st.ReasoningIndex = st.NextIndex
|
||||||
st.NextIndex++
|
st.NextIndex++
|
||||||
st.ReasoningItemID = fmt.Sprintf("rs_%s_%d", st.ResponseID, st.ReasoningIndex)
|
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 := []byte(`{"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.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", st.ReasoningIndex)
|
item, _ = sjson.SetBytes(item, "output_index", st.ReasoningIndex)
|
||||||
item, _ = sjson.Set(item, "item.id", st.ReasoningItemID)
|
item, _ = sjson.SetBytes(item, "item.id", st.ReasoningItemID)
|
||||||
item, _ = sjson.Set(item, "item.encrypted_content", st.ReasoningEnc)
|
item, _ = sjson.SetBytes(item, "item.encrypted_content", st.ReasoningEnc)
|
||||||
out = append(out, emitEvent("response.output_item.added", item))
|
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 := []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.Set(partAdded, "sequence_number", nextSeq())
|
partAdded, _ = sjson.SetBytes(partAdded, "sequence_number", nextSeq())
|
||||||
partAdded, _ = sjson.Set(partAdded, "item_id", st.ReasoningItemID)
|
partAdded, _ = sjson.SetBytes(partAdded, "item_id", st.ReasoningItemID)
|
||||||
partAdded, _ = sjson.Set(partAdded, "output_index", st.ReasoningIndex)
|
partAdded, _ = sjson.SetBytes(partAdded, "output_index", st.ReasoningIndex)
|
||||||
out = append(out, emitEvent("response.reasoning_summary_part.added", partAdded))
|
out = append(out, emitEvent("response.reasoning_summary_part.added", partAdded))
|
||||||
}
|
}
|
||||||
if t := part.Get("text"); t.Exists() && t.String() != "" {
|
if t := part.Get("text"); t.Exists() && t.String() != "" {
|
||||||
st.ReasoningBuf.WriteString(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 := []byte(`{"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.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", st.ReasoningItemID)
|
msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningItemID)
|
||||||
msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex)
|
msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex)
|
||||||
msg, _ = sjson.Set(msg, "delta", t.String())
|
msg, _ = sjson.SetBytes(msg, "delta", t.String())
|
||||||
out = append(out, emitEvent("response.reasoning_summary_text.delta", msg))
|
out = append(out, emitEvent("response.reasoning_summary_text.delta", msg))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -276,25 +277,25 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
st.MsgIndex = st.NextIndex
|
st.MsgIndex = st.NextIndex
|
||||||
st.NextIndex++
|
st.NextIndex++
|
||||||
st.CurrentMsgID = fmt.Sprintf("msg_%s_0", st.ResponseID)
|
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 := []byte(`{"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.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", st.MsgIndex)
|
item, _ = sjson.SetBytes(item, "output_index", st.MsgIndex)
|
||||||
item, _ = sjson.Set(item, "item.id", st.CurrentMsgID)
|
item, _ = sjson.SetBytes(item, "item.id", st.CurrentMsgID)
|
||||||
out = append(out, emitEvent("response.output_item.added", item))
|
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 := []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.Set(partAdded, "sequence_number", nextSeq())
|
partAdded, _ = sjson.SetBytes(partAdded, "sequence_number", nextSeq())
|
||||||
partAdded, _ = sjson.Set(partAdded, "item_id", st.CurrentMsgID)
|
partAdded, _ = sjson.SetBytes(partAdded, "item_id", st.CurrentMsgID)
|
||||||
partAdded, _ = sjson.Set(partAdded, "output_index", st.MsgIndex)
|
partAdded, _ = sjson.SetBytes(partAdded, "output_index", st.MsgIndex)
|
||||||
out = append(out, emitEvent("response.content_part.added", partAdded))
|
out = append(out, emitEvent("response.content_part.added", partAdded))
|
||||||
st.ItemTextBuf.Reset()
|
st.ItemTextBuf.Reset()
|
||||||
}
|
}
|
||||||
st.TextBuf.WriteString(t.String())
|
st.TextBuf.WriteString(t.String())
|
||||||
st.ItemTextBuf.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 := []byte(`{"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.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", st.CurrentMsgID)
|
msg, _ = sjson.SetBytes(msg, "item_id", st.CurrentMsgID)
|
||||||
msg, _ = sjson.Set(msg, "output_index", st.MsgIndex)
|
msg, _ = sjson.SetBytes(msg, "output_index", st.MsgIndex)
|
||||||
msg, _ = sjson.Set(msg, "delta", t.String())
|
msg, _ = sjson.SetBytes(msg, "delta", t.String())
|
||||||
out = append(out, emitEvent("response.output_text.delta", msg))
|
out = append(out, emitEvent("response.output_text.delta", msg))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -326,41 +327,41 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Emit item.added for function call
|
// 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 := []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.Set(item, "sequence_number", nextSeq())
|
item, _ = sjson.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", idx)
|
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||||
item, _ = sjson.Set(item, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
item, _ = sjson.Set(item, "item.call_id", st.FuncCallIDs[idx])
|
item, _ = sjson.SetBytes(item, "item.call_id", st.FuncCallIDs[idx])
|
||||||
item, _ = sjson.Set(item, "item.name", name)
|
item, _ = sjson.SetBytes(item, "item.name", name)
|
||||||
out = append(out, emitEvent("response.output_item.added", item))
|
out = append(out, emitEvent("response.output_item.added", item))
|
||||||
|
|
||||||
// Emit arguments delta (full args in one chunk).
|
// Emit arguments delta (full args in one chunk).
|
||||||
// When Gemini omits args, emit "{}" to keep Responses streaming event order consistent.
|
// When Gemini omits args, emit "{}" to keep Responses streaming event order consistent.
|
||||||
if argsJSON != "" {
|
if argsJSON != "" {
|
||||||
ad := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`
|
ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`)
|
||||||
ad, _ = sjson.Set(ad, "sequence_number", nextSeq())
|
ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq())
|
||||||
ad, _ = sjson.Set(ad, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
ad, _ = sjson.Set(ad, "output_index", idx)
|
ad, _ = sjson.SetBytes(ad, "output_index", idx)
|
||||||
ad, _ = sjson.Set(ad, "delta", argsJSON)
|
ad, _ = sjson.SetBytes(ad, "delta", argsJSON)
|
||||||
out = append(out, emitEvent("response.function_call_arguments.delta", ad))
|
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.
|
// Gemini emits the full function call payload at once, so we can finalize it immediately.
|
||||||
if !st.FuncDone[idx] {
|
if !st.FuncDone[idx] {
|
||||||
fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`
|
fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`)
|
||||||
fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq())
|
fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq())
|
||||||
fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
fcDone, _ = sjson.Set(fcDone, "output_index", idx)
|
fcDone, _ = sjson.SetBytes(fcDone, "output_index", idx)
|
||||||
fcDone, _ = sjson.Set(fcDone, "arguments", argsJSON)
|
fcDone, _ = sjson.SetBytes(fcDone, "arguments", argsJSON)
|
||||||
out = append(out, emitEvent("response.function_call_arguments.done", fcDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", idx)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.arguments", argsJSON)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", argsJSON)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.call_id", st.FuncCallIDs[idx])
|
itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", st.FuncCallIDs[idx])
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[idx])
|
itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[idx])
|
||||||
out = append(out, emitEvent("response.output_item.done", itemDone))
|
out = append(out, emitEvent("response.output_item.done", itemDone))
|
||||||
|
|
||||||
st.FuncDone[idx] = true
|
st.FuncDone[idx] = true
|
||||||
@@ -401,20 +402,20 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
if b := st.FuncArgsBuf[idx]; b != nil && b.Len() > 0 {
|
if b := st.FuncArgsBuf[idx]; b != nil && b.Len() > 0 {
|
||||||
args = b.String()
|
args = b.String()
|
||||||
}
|
}
|
||||||
fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`
|
fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`)
|
||||||
fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq())
|
fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq())
|
||||||
fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
fcDone, _ = sjson.Set(fcDone, "output_index", idx)
|
fcDone, _ = sjson.SetBytes(fcDone, "output_index", idx)
|
||||||
fcDone, _ = sjson.Set(fcDone, "arguments", args)
|
fcDone, _ = sjson.SetBytes(fcDone, "arguments", args)
|
||||||
out = append(out, emitEvent("response.function_call_arguments.done", fcDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", idx)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", st.FuncCallIDs[idx]))
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.arguments", args)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.call_id", st.FuncCallIDs[idx])
|
itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", st.FuncCallIDs[idx])
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[idx])
|
itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[idx])
|
||||||
out = append(out, emitEvent("response.output_item.done", itemDone))
|
out = append(out, emitEvent("response.output_item.done", itemDone))
|
||||||
|
|
||||||
st.FuncDone[idx] = true
|
st.FuncDone[idx] = true
|
||||||
@@ -424,91 +425,91 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
// Reasoning already finalized above if present
|
// Reasoning already finalized above if present
|
||||||
|
|
||||||
// Build response.completed with aggregated outputs and request echo fields
|
// 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 := []byte(`{"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.SetBytes(completed, "sequence_number", nextSeq())
|
||||||
completed, _ = sjson.Set(completed, "response.id", st.ResponseID)
|
completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID)
|
||||||
completed, _ = sjson.Set(completed, "response.created_at", st.CreatedAt)
|
completed, _ = sjson.SetBytes(completed, "response.created_at", st.CreatedAt)
|
||||||
|
|
||||||
if reqJSON := pickRequestJSON(originalRequestRawJSON, requestRawJSON); len(reqJSON) > 0 {
|
if reqJSON := pickRequestJSON(originalRequestRawJSON, requestRawJSON); len(reqJSON) > 0 {
|
||||||
req := unwrapRequestRoot(gjson.ParseBytes(reqJSON))
|
req := unwrapRequestRoot(gjson.ParseBytes(reqJSON))
|
||||||
if v := req.Get("instructions"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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.
|
// Compose outputs in output_index order.
|
||||||
outputsWrapper := `{"arr":[]}`
|
outputsWrapper := []byte(`{"arr":[]}`)
|
||||||
for idx := 0; idx < st.NextIndex; idx++ {
|
for idx := 0; idx < st.NextIndex; idx++ {
|
||||||
if st.ReasoningOpened && idx == st.ReasoningIndex {
|
if st.ReasoningOpened && idx == st.ReasoningIndex {
|
||||||
item := `{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]}`
|
item := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[{"type":"summary_text","text":""}]}`)
|
||||||
item, _ = sjson.Set(item, "id", st.ReasoningItemID)
|
item, _ = sjson.SetBytes(item, "id", st.ReasoningItemID)
|
||||||
item, _ = sjson.Set(item, "encrypted_content", st.ReasoningEnc)
|
item, _ = sjson.SetBytes(item, "encrypted_content", st.ReasoningEnc)
|
||||||
item, _ = sjson.Set(item, "summary.0.text", st.ReasoningBuf.String())
|
item, _ = sjson.SetBytes(item, "summary.0.text", st.ReasoningBuf.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if st.MsgOpened && idx == st.MsgIndex {
|
if st.MsgOpened && idx == st.MsgIndex {
|
||||||
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
item := []byte(`{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`)
|
||||||
item, _ = sjson.Set(item, "id", st.CurrentMsgID)
|
item, _ = sjson.SetBytes(item, "id", st.CurrentMsgID)
|
||||||
item, _ = sjson.Set(item, "content.0.text", st.TextBuf.String())
|
item, _ = sjson.SetBytes(item, "content.0.text", st.TextBuf.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,40 +518,40 @@ func ConvertGeminiResponseToOpenAIResponses(_ context.Context, modelName string,
|
|||||||
if b := st.FuncArgsBuf[idx]; b != nil && b.Len() > 0 {
|
if b := st.FuncArgsBuf[idx]; b != nil && b.Len() > 0 {
|
||||||
args = b.String()
|
args = b.String()
|
||||||
}
|
}
|
||||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
item, _ = sjson.Set(item, "arguments", args)
|
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||||
item, _ = sjson.Set(item, "call_id", callID)
|
item, _ = sjson.SetBytes(item, "call_id", callID)
|
||||||
item, _ = sjson.Set(item, "name", st.FuncNames[idx])
|
item, _ = sjson.SetBytes(item, "name", st.FuncNames[idx])
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// usage mapping
|
// usage mapping
|
||||||
if um := root.Get("usageMetadata"); um.Exists() {
|
if um := root.Get("usageMetadata"); um.Exists() {
|
||||||
// input tokens = prompt only (thoughts go to output)
|
// input tokens = prompt only (thoughts go to output)
|
||||||
input := um.Get("promptTokenCount").Int()
|
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.
|
// 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
|
// output tokens
|
||||||
if v := um.Get("candidatesTokenCount"); v.Exists() {
|
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 {
|
} 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() {
|
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 {
|
} 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() {
|
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 {
|
} 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.
|
// 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 := gjson.ParseBytes(rawJSON)
|
||||||
root = unwrapGeminiResponseRoot(root)
|
root = unwrapGeminiResponseRoot(root)
|
||||||
|
|
||||||
// Base response scaffold
|
// 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: prefer provider responseId, otherwise synthesize
|
||||||
id := root.Get("responseId").String()
|
id := root.Get("responseId").String()
|
||||||
@@ -577,7 +578,7 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
if !strings.HasPrefix(id, "resp_") {
|
if !strings.HasPrefix(id, "resp_") {
|
||||||
id = fmt.Sprintf("resp_%s", id)
|
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
|
// created_at: map from createTime if available
|
||||||
createdAt := time.Now().Unix()
|
createdAt := time.Now().Unix()
|
||||||
@@ -586,75 +587,75 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
createdAt = t.Unix()
|
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
|
// Echo request fields when present; fallback model from response modelVersion
|
||||||
if reqJSON := pickRequestJSON(originalRequestRawJSON, requestRawJSON); len(reqJSON) > 0 {
|
if reqJSON := pickRequestJSON(originalRequestRawJSON, requestRawJSON); len(reqJSON) > 0 {
|
||||||
req := unwrapRequestRoot(gjson.ParseBytes(reqJSON))
|
req := unwrapRequestRoot(gjson.ParseBytes(reqJSON))
|
||||||
if v := req.Get("instructions"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
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() {
|
} 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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
} 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
|
// Build outputs from candidates[0].content.parts
|
||||||
@@ -668,12 +669,12 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
if haveOutput {
|
if haveOutput {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp, _ = sjson.SetRaw(resp, "output", "[]")
|
resp, _ = sjson.SetRawBytes(resp, "output", []byte("[]"))
|
||||||
haveOutput = true
|
haveOutput = true
|
||||||
}
|
}
|
||||||
appendOutput := func(itemJSON string) {
|
appendOutput := func(itemJSON []byte) {
|
||||||
ensureOutput()
|
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() {
|
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()
|
name := fc.Get("name").String()
|
||||||
args := fc.Get("args")
|
args := fc.Get("args")
|
||||||
callID := fmt.Sprintf("call_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&funcCallIDCounter, 1))
|
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 := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("fc_%s", callID))
|
itemJSON, _ = sjson.SetBytes(itemJSON, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "call_id", callID)
|
itemJSON, _ = sjson.SetBytes(itemJSON, "call_id", callID)
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "name", name)
|
itemJSON, _ = sjson.SetBytes(itemJSON, "name", name)
|
||||||
argsStr := ""
|
argsStr := ""
|
||||||
if args.Exists() {
|
if args.Exists() {
|
||||||
argsStr = args.Raw
|
argsStr = args.Raw
|
||||||
}
|
}
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "arguments", argsStr)
|
itemJSON, _ = sjson.SetBytes(itemJSON, "arguments", argsStr)
|
||||||
appendOutput(itemJSON)
|
appendOutput(itemJSON)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -715,23 +716,23 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
// Reasoning output item
|
// Reasoning output item
|
||||||
if reasoningText.Len() > 0 || reasoningEncrypted != "" {
|
if reasoningText.Len() > 0 || reasoningEncrypted != "" {
|
||||||
rid := strings.TrimPrefix(id, "resp_")
|
rid := strings.TrimPrefix(id, "resp_")
|
||||||
itemJSON := `{"id":"","type":"reasoning","encrypted_content":""}`
|
itemJSON := []byte(`{"id":"","type":"reasoning","encrypted_content":""}`)
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "id", fmt.Sprintf("rs_%s", rid))
|
itemJSON, _ = sjson.SetBytes(itemJSON, "id", fmt.Sprintf("rs_%s", rid))
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "encrypted_content", reasoningEncrypted)
|
itemJSON, _ = sjson.SetBytes(itemJSON, "encrypted_content", reasoningEncrypted)
|
||||||
if reasoningText.Len() > 0 {
|
if reasoningText.Len() > 0 {
|
||||||
summaryJSON := `{"type":"summary_text","text":""}`
|
summaryJSON := []byte(`{"type":"summary_text","text":""}`)
|
||||||
summaryJSON, _ = sjson.Set(summaryJSON, "text", reasoningText.String())
|
summaryJSON, _ = sjson.SetBytes(summaryJSON, "text", reasoningText.String())
|
||||||
itemJSON, _ = sjson.SetRaw(itemJSON, "summary", "[]")
|
itemJSON, _ = sjson.SetRawBytes(itemJSON, "summary", []byte(`[]`))
|
||||||
itemJSON, _ = sjson.SetRaw(itemJSON, "summary.-1", summaryJSON)
|
itemJSON, _ = sjson.SetRawBytes(itemJSON, "summary.-1", summaryJSON)
|
||||||
}
|
}
|
||||||
appendOutput(itemJSON)
|
appendOutput(itemJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assistant message output item
|
// Assistant message output item
|
||||||
if haveMessage {
|
if haveMessage {
|
||||||
itemJSON := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
itemJSON := []byte(`{"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.SetBytes(itemJSON, "id", fmt.Sprintf("msg_%s_0", strings.TrimPrefix(id, "resp_")))
|
||||||
itemJSON, _ = sjson.Set(itemJSON, "content.0.text", messageText.String())
|
itemJSON, _ = sjson.SetBytes(itemJSON, "content.0.text", messageText.String())
|
||||||
appendOutput(itemJSON)
|
appendOutput(itemJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,18 +740,18 @@ func ConvertGeminiResponseToOpenAIResponsesNonStream(_ context.Context, _ string
|
|||||||
if um := root.Get("usageMetadata"); um.Exists() {
|
if um := root.Get("usageMetadata"); um.Exists() {
|
||||||
// input tokens = prompt only (thoughts go to output)
|
// input tokens = prompt only (thoughts go to output)
|
||||||
input := um.Get("promptTokenCount").Int()
|
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.
|
// 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
|
// output tokens
|
||||||
if v := um.Get("candidatesTokenCount"); v.Exists() {
|
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() {
|
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() {
|
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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
"github.com/tidwall/gjson"
|
"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()
|
t.Helper()
|
||||||
|
|
||||||
lines := strings.Split(chunk, "\n")
|
lines := strings.Split(string(chunk), "\n")
|
||||||
if len(lines) < 2 {
|
if len(lines) < 2 {
|
||||||
t.Fatalf("unexpected SSE chunk: %q", chunk)
|
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}`)
|
originalReq := []byte(`{"instructions":"test instructions","model":"gpt-5","max_output_tokens":123}`)
|
||||||
|
|
||||||
var param any
|
var param any
|
||||||
var out []string
|
var out [][]byte
|
||||||
for _, line := range in {
|
for _, line := range in {
|
||||||
out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", originalReq, nil, []byte(line), ¶m)...)
|
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 param any
|
||||||
var out []string
|
var out [][]byte
|
||||||
for _, line := range in {
|
for _, line := range in {
|
||||||
out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...)
|
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 param any
|
||||||
var out []string
|
var out [][]byte
|
||||||
for _, line := range in {
|
for _, line := range in {
|
||||||
out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...)
|
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 param any
|
||||||
var out []string
|
var out [][]byte
|
||||||
for _, line := range in {
|
for _, line := range in {
|
||||||
out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...)
|
out = append(out, ConvertGeminiResponseToOpenAIResponses(context.Background(), "test-model", nil, nil, []byte(line), ¶m)...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import (
|
|||||||
// ConvertKiroStreamToClaude converts Kiro streaming response to Claude format.
|
// ConvertKiroStreamToClaude converts Kiro streaming response to Claude format.
|
||||||
// Kiro executor already generates complete SSE format with "event:" prefix,
|
// Kiro executor already generates complete SSE format with "event:" prefix,
|
||||||
// so this is a simple pass-through.
|
// so this is a simple pass-through.
|
||||||
func ConvertKiroStreamToClaude(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []string {
|
func ConvertKiroStreamToClaude(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) [][]byte {
|
||||||
return []string{string(rawResponse)}
|
return [][]byte{rawResponse}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertKiroNonStreamToClaude converts Kiro non-streaming response to Claude format.
|
// ConvertKiroNonStreamToClaude converts Kiro non-streaming response to Claude format.
|
||||||
// The response is already in Claude format, so this is a pass-through.
|
// The response is already in Claude format, so this is a pass-through.
|
||||||
func ConvertKiroNonStreamToClaude(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) string {
|
func ConvertKiroNonStreamToClaude(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []byte {
|
||||||
return string(rawResponse)
|
return rawResponse
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import (
|
|||||||
// OpenAI SSE format:
|
// OpenAI SSE format:
|
||||||
// - data: {"id":"...","object":"chat.completion.chunk",...}
|
// - data: {"id":"...","object":"chat.completion.chunk",...}
|
||||||
// - data: [DONE]
|
// - data: [DONE]
|
||||||
func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []string {
|
func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) [][]byte {
|
||||||
// Initialize state if needed
|
// Initialize state if needed
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = NewOpenAIStreamState(model)
|
*param = NewOpenAIStreamState(model)
|
||||||
@@ -64,13 +64,13 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
}
|
}
|
||||||
|
|
||||||
if eventData == "" {
|
if eventData == "" {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the event data as JSON
|
// Parse the event data as JSON
|
||||||
eventJSON := gjson.Parse(eventData)
|
eventJSON := gjson.Parse(eventData)
|
||||||
if !eventJSON.Exists() {
|
if !eventJSON.Exists() {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine event type from JSON if not already set
|
// Determine event type from JSON if not already set
|
||||||
@@ -78,13 +78,13 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
eventType = eventJSON.Get("type").String()
|
eventType = eventJSON.Get("type").String()
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []string
|
var results [][]byte
|
||||||
|
|
||||||
switch eventType {
|
switch eventType {
|
||||||
case "message_start":
|
case "message_start":
|
||||||
// Send first chunk with role
|
// Send first chunk with role
|
||||||
firstChunk := BuildOpenAISSEFirstChunk(state)
|
firstChunk := BuildOpenAISSEFirstChunk(state)
|
||||||
results = append(results, firstChunk)
|
results = append(results, []byte(firstChunk))
|
||||||
|
|
||||||
case "content_block_start":
|
case "content_block_start":
|
||||||
// Check block type
|
// Check block type
|
||||||
@@ -99,7 +99,7 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
toolUseID := eventJSON.Get("content_block.id").String()
|
toolUseID := eventJSON.Get("content_block.id").String()
|
||||||
toolName := eventJSON.Get("content_block.name").String()
|
toolName := eventJSON.Get("content_block.name").String()
|
||||||
chunk := BuildOpenAISSEToolCallStart(state, toolUseID, toolName)
|
chunk := BuildOpenAISSEToolCallStart(state, toolUseID, toolName)
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
state.ToolCallIndex++
|
state.ToolCallIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,14 +110,14 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
textDelta := eventJSON.Get("delta.text").String()
|
textDelta := eventJSON.Get("delta.text").String()
|
||||||
if textDelta != "" {
|
if textDelta != "" {
|
||||||
chunk := BuildOpenAISSETextDelta(state, textDelta)
|
chunk := BuildOpenAISSETextDelta(state, textDelta)
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
}
|
}
|
||||||
case "thinking_delta":
|
case "thinking_delta":
|
||||||
// Convert thinking to reasoning_content for o1-style compatibility
|
// Convert thinking to reasoning_content for o1-style compatibility
|
||||||
thinkingDelta := eventJSON.Get("delta.thinking").String()
|
thinkingDelta := eventJSON.Get("delta.thinking").String()
|
||||||
if thinkingDelta != "" {
|
if thinkingDelta != "" {
|
||||||
chunk := BuildOpenAISSEReasoningDelta(state, thinkingDelta)
|
chunk := BuildOpenAISSEReasoningDelta(state, thinkingDelta)
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
}
|
}
|
||||||
case "input_json_delta":
|
case "input_json_delta":
|
||||||
// Tool call arguments delta
|
// Tool call arguments delta
|
||||||
@@ -126,7 +126,7 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
// Get the tool index from content block index
|
// Get the tool index from content block index
|
||||||
blockIndex := int(eventJSON.Get("index").Int())
|
blockIndex := int(eventJSON.Get("index").Int())
|
||||||
chunk := BuildOpenAISSEToolCallArgumentsDelta(state, partialJSON, blockIndex-1) // Adjust for 0-based tool index
|
chunk := BuildOpenAISSEToolCallArgumentsDelta(state, partialJSON, blockIndex-1) // Adjust for 0-based tool index
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
finishReason := mapKiroStopReasonToOpenAI(stopReason)
|
finishReason := mapKiroStopReasonToOpenAI(stopReason)
|
||||||
if finishReason != "" {
|
if finishReason != "" {
|
||||||
chunk := BuildOpenAISSEFinish(state, finishReason)
|
chunk := BuildOpenAISSEFinish(state, finishReason)
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract usage if present
|
// Extract usage if present
|
||||||
@@ -152,7 +152,7 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
TotalTokens: inputTokens + outputTokens,
|
TotalTokens: inputTokens + outputTokens,
|
||||||
}
|
}
|
||||||
chunk := BuildOpenAISSEUsage(state, usageInfo)
|
chunk := BuildOpenAISSEUsage(state, usageInfo)
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
}
|
}
|
||||||
|
|
||||||
case "message_stop":
|
case "message_stop":
|
||||||
@@ -171,7 +171,7 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
TotalTokens: inputTokens + outputTokens,
|
TotalTokens: inputTokens + outputTokens,
|
||||||
}
|
}
|
||||||
chunk := BuildOpenAISSEUsage(state, usageInfo)
|
chunk := BuildOpenAISSEUsage(state, usageInfo)
|
||||||
results = append(results, chunk)
|
results = append(results, []byte(chunk))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ func ConvertKiroStreamToOpenAI(ctx context.Context, model string, originalReques
|
|||||||
// ConvertKiroNonStreamToOpenAI converts Kiro non-streaming response to OpenAI format.
|
// ConvertKiroNonStreamToOpenAI converts Kiro non-streaming response to OpenAI format.
|
||||||
// The Kiro executor returns Claude-compatible JSON responses, so this function translates
|
// The Kiro executor returns Claude-compatible JSON responses, so this function translates
|
||||||
// from Claude format to OpenAI format.
|
// from Claude format to OpenAI format.
|
||||||
func ConvertKiroNonStreamToOpenAI(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) string {
|
func ConvertKiroNonStreamToOpenAI(ctx context.Context, model string, originalRequest, request, rawResponse []byte, param *any) []byte {
|
||||||
// Parse the Claude-format response
|
// Parse the Claude-format response
|
||||||
response := gjson.ParseBytes(rawResponse)
|
response := gjson.ParseBytes(rawResponse)
|
||||||
|
|
||||||
@@ -237,7 +237,7 @@ func ConvertKiroNonStreamToOpenAI(ctx context.Context, model string, originalReq
|
|||||||
|
|
||||||
// Build OpenAI response with reasoning_content support
|
// Build OpenAI response with reasoning_content support
|
||||||
openaiResponse := BuildOpenAIResponseWithReasoning(content, reasoningContent, toolUses, model, usageInfo, stopReason)
|
openaiResponse := BuildOpenAIResponseWithReasoning(content, reasoningContent, toolUses, model, usageInfo, stopReason)
|
||||||
return string(openaiResponse)
|
return openaiResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseClaudeEvent parses a Claude SSE event and returns the event type and data
|
// ParseClaudeEvent parses a Claude SSE event and returns the event type and data
|
||||||
|
|||||||
@@ -19,23 +19,23 @@ import (
|
|||||||
func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
// Base OpenAI Chat Completions API template
|
// Base OpenAI Chat Completions API template
|
||||||
out := `{"model":"","messages":[]}`
|
out := []byte(`{"model":"","messages":[]}`)
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Model mapping
|
// Model mapping
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// Max tokens
|
// Max tokens
|
||||||
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
|
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
|
// Temperature
|
||||||
if temp := root.Get("temperature"); temp.Exists() {
|
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
|
} 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
|
// Stop sequences -> stop
|
||||||
@@ -48,16 +48,16 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
})
|
})
|
||||||
if len(stops) > 0 {
|
if len(stops) > 0 {
|
||||||
if len(stops) == 1 {
|
if len(stops) == 1 {
|
||||||
out, _ = sjson.Set(out, "stop", stops[0])
|
out, _ = sjson.SetBytes(out, "stop", stops[0])
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "stop", stops)
|
out, _ = sjson.SetBytes(out, "stop", stops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream
|
// Stream
|
||||||
out, _ = sjson.Set(out, "stream", stream)
|
out, _ = sjson.SetBytes(out, "stream", stream)
|
||||||
|
|
||||||
// Thinking: Convert Claude thinking.budget_tokens to OpenAI reasoning_effort
|
// Thinking: Convert Claude thinking.budget_tokens to OpenAI reasoning_effort
|
||||||
if thinkingConfig := root.Get("thinking"); thinkingConfig.Exists() && thinkingConfig.IsObject() {
|
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() {
|
if budgetTokens := thinkingConfig.Get("budget_tokens"); budgetTokens.Exists() {
|
||||||
budget := int(budgetTokens.Int())
|
budget := int(budgetTokens.Int())
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
if effort, ok := thinking.ConvertBudgetToLevel(budget); ok && effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No budget_tokens specified, default to "auto" for enabled thinking
|
// No budget_tokens specified, default to "auto" for enabled thinking
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(-1); ok && effort != "" {
|
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":
|
case "adaptive", "auto":
|
||||||
@@ -83,30 +83,30 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
effort = strings.ToLower(strings.TrimSpace(v.String()))
|
||||||
}
|
}
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", string(thinking.LevelXHigh))
|
out, _ = sjson.SetBytes(out, "reasoning_effort", string(thinking.LevelXHigh))
|
||||||
}
|
}
|
||||||
case "disabled":
|
case "disabled":
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(0); ok && effort != "" {
|
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
|
// Process messages and system
|
||||||
var messagesJSON = "[]"
|
messagesJSON := []byte(`[]`)
|
||||||
|
|
||||||
// Handle system message first
|
// Handle system message first
|
||||||
systemMsgJSON := `{"role":"system","content":[]}`
|
systemMsgJSON := []byte(`{"role":"system","content":[]}`)
|
||||||
hasSystemContent := false
|
hasSystemContent := false
|
||||||
if system := root.Get("system"); system.Exists() {
|
if system := root.Get("system"); system.Exists() {
|
||||||
if system.Type == gjson.String {
|
if system.Type == gjson.String {
|
||||||
if system.String() != "" {
|
if system.String() != "" {
|
||||||
oldSystem := `{"type":"text","text":""}`
|
oldSystem := []byte(`{"type":"text","text":""}`)
|
||||||
oldSystem, _ = sjson.Set(oldSystem, "text", system.String())
|
oldSystem, _ = sjson.SetBytes(oldSystem, "text", system.String())
|
||||||
systemMsgJSON, _ = sjson.SetRaw(systemMsgJSON, "content.-1", oldSystem)
|
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", oldSystem)
|
||||||
hasSystemContent = true
|
hasSystemContent = true
|
||||||
}
|
}
|
||||||
} else if system.Type == gjson.JSON {
|
} else if system.Type == gjson.JSON {
|
||||||
@@ -114,7 +114,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
systemResults := system.Array()
|
systemResults := system.Array()
|
||||||
for i := 0; i < len(systemResults); i++ {
|
for i := 0; i < len(systemResults); i++ {
|
||||||
if contentItem, ok := convertClaudeContentPart(systemResults[i]); ok {
|
if contentItem, ok := convertClaudeContentPart(systemResults[i]); ok {
|
||||||
systemMsgJSON, _ = sjson.SetRaw(systemMsgJSON, "content.-1", contentItem)
|
systemMsgJSON, _ = sjson.SetRawBytes(systemMsgJSON, "content.-1", []byte(contentItem))
|
||||||
hasSystemContent = true
|
hasSystemContent = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,7 +123,7 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
// Only add system message if it has content
|
// Only add system message if it has content
|
||||||
if hasSystemContent {
|
if hasSystemContent {
|
||||||
messagesJSON, _ = sjson.SetRaw(messagesJSON, "-1", systemMsgJSON)
|
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", systemMsgJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process Anthropic messages
|
// Process Anthropic messages
|
||||||
@@ -134,10 +134,10 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
// Handle content
|
// Handle content
|
||||||
if contentResult.Exists() && contentResult.IsArray() {
|
if contentResult.Exists() && contentResult.IsArray() {
|
||||||
var contentItems []string
|
contentItems := make([][]byte, 0)
|
||||||
var reasoningParts []string // Accumulate thinking text for reasoning_content
|
var reasoningParts []string // Accumulate thinking text for reasoning_content
|
||||||
var toolCalls []interface{}
|
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 {
|
contentResult.ForEach(func(_, part gjson.Result) bool {
|
||||||
partType := part.Get("type").String()
|
partType := part.Get("type").String()
|
||||||
@@ -159,35 +159,35 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
|
|
||||||
case "text", "image":
|
case "text", "image":
|
||||||
if contentItem, ok := convertClaudeContentPart(part); ok {
|
if contentItem, ok := convertClaudeContentPart(part); ok {
|
||||||
contentItems = append(contentItems, contentItem)
|
contentItems = append(contentItems, []byte(contentItem))
|
||||||
}
|
}
|
||||||
|
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
// Only allow tool_use -> tool_calls for assistant messages (security: prevent injection).
|
// Only allow tool_use -> tool_calls for assistant messages (security: prevent injection).
|
||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
toolCallJSON := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
|
toolCallJSON := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "id", part.Get("id").String())
|
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "id", part.Get("id").String())
|
||||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.name", part.Get("name").String())
|
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.name", part.Get("name").String())
|
||||||
|
|
||||||
// Convert input to arguments JSON string
|
// Convert input to arguments JSON string
|
||||||
if input := part.Get("input"); input.Exists() {
|
if input := part.Get("input"); input.Exists() {
|
||||||
toolCallJSON, _ = sjson.Set(toolCallJSON, "function.arguments", input.Raw)
|
toolCallJSON, _ = sjson.SetBytes(toolCallJSON, "function.arguments", input.Raw)
|
||||||
} else {
|
} 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":
|
case "tool_result":
|
||||||
// Collect tool_result to emit after the main message (ensures tool results follow tool_calls)
|
// Collect tool_result to emit after the main message (ensures tool results follow tool_calls)
|
||||||
toolResultJSON := `{"role":"tool","tool_call_id":"","content":""}`
|
toolResultJSON := []byte(`{"role":"tool","tool_call_id":"","content":""}`)
|
||||||
toolResultJSON, _ = sjson.Set(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String())
|
toolResultJSON, _ = sjson.SetBytes(toolResultJSON, "tool_call_id", part.Get("tool_use_id").String())
|
||||||
toolResultContent, toolResultContentRaw := convertClaudeToolResultContent(part.Get("content"))
|
toolResultContent, toolResultContentRaw := convertClaudeToolResultContent(part.Get("content"))
|
||||||
if toolResultContentRaw {
|
if toolResultContentRaw {
|
||||||
toolResultJSON, _ = sjson.SetRaw(toolResultJSON, "content", toolResultContent)
|
toolResultJSON, _ = sjson.SetRawBytes(toolResultJSON, "content", []byte(toolResultContent))
|
||||||
} else {
|
} else {
|
||||||
toolResultJSON, _ = sjson.Set(toolResultJSON, "content", toolResultContent)
|
toolResultJSON, _ = sjson.SetBytes(toolResultJSON, "content", toolResultContent)
|
||||||
}
|
}
|
||||||
toolResults = append(toolResults, toolResultJSON)
|
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),
|
// Therefore, we emit tool_result messages FIRST (they respond to the previous assistant's tool_calls),
|
||||||
// then emit the current message's content.
|
// then emit the current message's content.
|
||||||
for _, toolResultJSON := range toolResults {
|
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
|
// 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
|
// This avoids splitting into multiple assistant messages which breaks OpenAI tool-call adjacency
|
||||||
if role == "assistant" {
|
if role == "assistant" {
|
||||||
if hasContent || hasReasoning || hasToolCalls {
|
if hasContent || hasReasoning || hasToolCalls {
|
||||||
msgJSON := `{"role":"assistant"}`
|
msgJSON := []byte(`{"role":"assistant"}`)
|
||||||
|
|
||||||
// Add content (as array if we have items, empty string if reasoning-only)
|
// Add content (as array if we have items, empty string if reasoning-only)
|
||||||
if hasContent {
|
if hasContent {
|
||||||
contentArrayJSON := "[]"
|
contentArrayJSON := []byte(`[]`)
|
||||||
for _, contentItem := range contentItems {
|
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 {
|
} else {
|
||||||
// Ensure content field exists for OpenAI compatibility
|
// Ensure content field exists for OpenAI compatibility
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "content", "")
|
msgJSON, _ = sjson.SetBytes(msgJSON, "content", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add reasoning_content if present
|
// Add reasoning_content if present
|
||||||
if hasReasoning {
|
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)
|
// Add tool_calls if present (in same message as content)
|
||||||
if hasToolCalls {
|
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 {
|
} else {
|
||||||
// For non-assistant roles: emit content message if we have content
|
// 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 the message only contains tool_results (no text/image), we still processed them above
|
||||||
if hasContent {
|
if hasContent {
|
||||||
msgJSON := `{"role":""}`
|
msgJSON := []byte(`{"role":""}`)
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
msgJSON, _ = sjson.SetBytes(msgJSON, "role", role)
|
||||||
|
|
||||||
contentArrayJSON := "[]"
|
contentArrayJSON := []byte(`[]`)
|
||||||
for _, contentItem := range contentItems {
|
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 {
|
} else if hasToolResults && !hasContent {
|
||||||
// tool_results already emitted above, no additional user message needed
|
// 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 {
|
} else if contentResult.Exists() && contentResult.Type == gjson.String {
|
||||||
// Simple string content
|
// Simple string content
|
||||||
msgJSON := `{"role":"","content":""}`
|
msgJSON := []byte(`{"role":"","content":""}`)
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "role", role)
|
msgJSON, _ = sjson.SetBytes(msgJSON, "role", role)
|
||||||
msgJSON, _ = sjson.Set(msgJSON, "content", contentResult.String())
|
msgJSON, _ = sjson.SetBytes(msgJSON, "content", contentResult.String())
|
||||||
messagesJSON, _ = sjson.Set(messagesJSON, "-1", gjson.Parse(msgJSON).Value())
|
messagesJSON, _ = sjson.SetRawBytes(messagesJSON, "-1", msgJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -274,30 +274,30 @@ func ConvertClaudeRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set messages
|
// Set messages
|
||||||
if gjson.Parse(messagesJSON).IsArray() && len(gjson.Parse(messagesJSON).Array()) > 0 {
|
if msgs := gjson.ParseBytes(messagesJSON); msgs.IsArray() && len(msgs.Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "messages", messagesJSON)
|
out, _ = sjson.SetRawBytes(out, "messages", messagesJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process tools - convert Anthropic tools to OpenAI functions
|
// Process tools - convert Anthropic tools to OpenAI functions
|
||||||
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
if tools := root.Get("tools"); tools.Exists() && tools.IsArray() {
|
||||||
var toolsJSON = "[]"
|
toolsJSON := []byte(`[]`)
|
||||||
|
|
||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
openAIToolJSON := `{"type":"function","function":{"name":"","description":""}}`
|
openAIToolJSON := []byte(`{"type":"function","function":{"name":"","description":""}}`)
|
||||||
openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.name", tool.Get("name").String())
|
openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.name", tool.Get("name").String())
|
||||||
openAIToolJSON, _ = sjson.Set(openAIToolJSON, "function.description", tool.Get("description").String())
|
openAIToolJSON, _ = sjson.SetBytes(openAIToolJSON, "function.description", tool.Get("description").String())
|
||||||
|
|
||||||
// Convert Anthropic input_schema to OpenAI function parameters
|
// Convert Anthropic input_schema to OpenAI function parameters
|
||||||
if inputSchema := tool.Get("input_schema"); inputSchema.Exists() {
|
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
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if gjson.Parse(toolsJSON).IsArray() && len(gjson.Parse(toolsJSON).Array()) > 0 {
|
if parsed := gjson.ParseBytes(toolsJSON); parsed.IsArray() && len(parsed.Array()) > 0 {
|
||||||
out, _ = sjson.SetRaw(out, "tools", toolsJSON)
|
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() {
|
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
||||||
switch toolChoice.Get("type").String() {
|
switch toolChoice.Get("type").String() {
|
||||||
case "auto":
|
case "auto":
|
||||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
|
||||||
case "any":
|
case "any":
|
||||||
out, _ = sjson.Set(out, "tool_choice", "required")
|
out, _ = sjson.SetBytes(out, "tool_choice", "required")
|
||||||
case "tool":
|
case "tool":
|
||||||
// Specific tool choice
|
// Specific tool choice
|
||||||
toolName := toolChoice.Get("name").String()
|
toolName := toolChoice.Get("name").String()
|
||||||
toolChoiceJSON := `{"type":"function","function":{"name":""}}`
|
toolChoiceJSON := []byte(`{"type":"function","function":{"name":""}}`)
|
||||||
toolChoiceJSON, _ = sjson.Set(toolChoiceJSON, "function.name", toolName)
|
toolChoiceJSON, _ = sjson.SetBytes(toolChoiceJSON, "function.name", toolName)
|
||||||
out, _ = sjson.SetRaw(out, "tool_choice", toolChoiceJSON)
|
out, _ = sjson.SetRawBytes(out, "tool_choice", toolChoiceJSON)
|
||||||
default:
|
default:
|
||||||
// Default to auto if not specified
|
// 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)
|
// Handle user parameter (for tracking)
|
||||||
if user := root.Get("user"); user.Exists() {
|
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) {
|
func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
||||||
@@ -337,9 +337,9 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
|||||||
if strings.TrimSpace(text) == "" {
|
if strings.TrimSpace(text) == "" {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
textContent := `{"type":"text","text":""}`
|
textContent := []byte(`{"type":"text","text":""}`)
|
||||||
textContent, _ = sjson.Set(textContent, "text", text)
|
textContent, _ = sjson.SetBytes(textContent, "text", text)
|
||||||
return textContent, true
|
return string(textContent), true
|
||||||
|
|
||||||
case "image":
|
case "image":
|
||||||
var imageURL string
|
var imageURL string
|
||||||
@@ -369,10 +369,10 @@ func convertClaudeContentPart(part gjson.Result) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
imageContent := `{"type":"image_url","image_url":{"url":""}}`
|
imageContent := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
imageContent, _ = sjson.Set(imageContent, "image_url.url", imageURL)
|
imageContent, _ = sjson.SetBytes(imageContent, "image_url.url", imageURL)
|
||||||
|
|
||||||
return imageContent, true
|
return string(imageContent), true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
@@ -390,26 +390,26 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
|||||||
|
|
||||||
if content.IsArray() {
|
if content.IsArray() {
|
||||||
var parts []string
|
var parts []string
|
||||||
contentJSON := "[]"
|
contentJSON := []byte(`[]`)
|
||||||
hasImagePart := false
|
hasImagePart := false
|
||||||
content.ForEach(func(_, item gjson.Result) bool {
|
content.ForEach(func(_, item gjson.Result) bool {
|
||||||
switch {
|
switch {
|
||||||
case item.Type == gjson.String:
|
case item.Type == gjson.String:
|
||||||
text := item.String()
|
text := item.String()
|
||||||
parts = append(parts, text)
|
parts = append(parts, text)
|
||||||
textContent := `{"type":"text","text":""}`
|
textContent := []byte(`{"type":"text","text":""}`)
|
||||||
textContent, _ = sjson.Set(textContent, "text", text)
|
textContent, _ = sjson.SetBytes(textContent, "text", text)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", textContent)
|
||||||
case item.IsObject() && item.Get("type").String() == "text":
|
case item.IsObject() && item.Get("type").String() == "text":
|
||||||
text := item.Get("text").String()
|
text := item.Get("text").String()
|
||||||
parts = append(parts, text)
|
parts = append(parts, text)
|
||||||
textContent := `{"type":"text","text":""}`
|
textContent := []byte(`{"type":"text","text":""}`)
|
||||||
textContent, _ = sjson.Set(textContent, "text", text)
|
textContent, _ = sjson.SetBytes(textContent, "text", text)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", textContent)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", textContent)
|
||||||
case item.IsObject() && item.Get("type").String() == "image":
|
case item.IsObject() && item.Get("type").String() == "image":
|
||||||
contentItem, ok := convertClaudeContentPart(item)
|
contentItem, ok := convertClaudeContentPart(item)
|
||||||
if ok {
|
if ok {
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", []byte(contentItem))
|
||||||
hasImagePart = true
|
hasImagePart = true
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, item.Raw)
|
parts = append(parts, item.Raw)
|
||||||
@@ -423,7 +423,7 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if hasImagePart {
|
if hasImagePart {
|
||||||
return contentJSON, true
|
return string(contentJSON), true
|
||||||
}
|
}
|
||||||
|
|
||||||
joined := strings.Join(parts, "\n\n")
|
joined := strings.Join(parts, "\n\n")
|
||||||
@@ -437,9 +437,9 @@ func convertClaudeToolResultContent(content gjson.Result) (string, bool) {
|
|||||||
if content.Get("type").String() == "image" {
|
if content.Get("type").String() == "image" {
|
||||||
contentItem, ok := convertClaudeContentPart(content)
|
contentItem, ok := convertClaudeContentPart(content)
|
||||||
if ok {
|
if ok {
|
||||||
contentJSON := "[]"
|
contentJSON := []byte(`[]`)
|
||||||
contentJSON, _ = sjson.SetRaw(contentJSON, "-1", contentItem)
|
contentJSON, _ = sjson.SetRawBytes(contentJSON, "-1", []byte(contentItem))
|
||||||
return contentJSON, true
|
return string(contentJSON), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if text := content.Get("text"); text.Exists() && text.Type == gjson.String {
|
if text := content.Get("text"); text.Exists() && text.Type == gjson.String {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ package claude
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
@@ -73,8 +73,8 @@ type ToolCallAccumulator struct {
|
|||||||
// - param: A pointer to a parameter object for the conversion.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an Anthropic-compatible JSON response.
|
// - [][]byte: A slice of byte chunks, each containing an Anthropic-compatible JSON response.
|
||||||
func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &ConvertOpenAIResponseToAnthropicParams{
|
*param = &ConvertOpenAIResponseToAnthropicParams{
|
||||||
MessageID: "",
|
MessageID: "",
|
||||||
@@ -97,7 +97,7 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.HasPrefix(rawJSON, dataTag) {
|
if !bytes.HasPrefix(rawJSON, dataTag) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
|
|
||||||
@@ -106,8 +106,7 @@ func ConvertOpenAIResponseToClaude(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is the [DONE] marker
|
// Check if this is the [DONE] marker
|
||||||
rawStr := strings.TrimSpace(string(rawJSON))
|
if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) {
|
||||||
if rawStr == "[DONE]" {
|
|
||||||
return convertOpenAIDoneToAnthropic((*param).(*ConvertOpenAIResponseToAnthropicParams))
|
return convertOpenAIDoneToAnthropic((*param).(*ConvertOpenAIResponseToAnthropicParams))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,9 +129,9 @@ func effectiveOpenAIFinishReason(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertOpenAIStreamingChunkToAnthropic converts OpenAI streaming chunk to Anthropic streaming events
|
// 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)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
var results []string
|
var results [][]byte
|
||||||
|
|
||||||
// Initialize parameters if needed
|
// Initialize parameters if needed
|
||||||
if param.MessageID == "" {
|
if param.MessageID == "" {
|
||||||
@@ -150,10 +149,10 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
if delta := root.Get("choices.0.delta"); delta.Exists() {
|
if delta := root.Get("choices.0.delta"); delta.Exists() {
|
||||||
if !param.MessageStarted {
|
if !param.MessageStarted {
|
||||||
// Send message_start event
|
// 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 := []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.Set(messageStartJSON, "message.id", param.MessageID)
|
messageStartJSON, _ = sjson.SetBytes(messageStartJSON, "message.id", param.MessageID)
|
||||||
messageStartJSON, _ = sjson.Set(messageStartJSON, "message.model", param.Model)
|
messageStartJSON, _ = sjson.SetBytes(messageStartJSON, "message.model", param.Model)
|
||||||
results = append(results, "event: message_start\ndata: "+messageStartJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_start", messageStartJSON, 2))
|
||||||
param.MessageStarted = true
|
param.MessageStarted = true
|
||||||
|
|
||||||
// Don't send content_block_start for text here - wait for actual content
|
// 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++
|
param.NextContentBlockIndex++
|
||||||
}
|
}
|
||||||
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":""}}`
|
||||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.ThinkingContentBlockIndex)
|
contentBlockStartJSONBytes := []byte(contentBlockStartJSON)
|
||||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", param.ThinkingContentBlockIndex)
|
||||||
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2))
|
||||||
param.ThinkingContentBlockStarted = true
|
param.ThinkingContentBlockStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
thinkingDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":""}}`
|
||||||
thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "index", param.ThinkingContentBlockIndex)
|
thinkingDeltaJSONBytes := []byte(thinkingDeltaJSON)
|
||||||
thinkingDeltaJSON, _ = sjson.Set(thinkingDeltaJSON, "delta.thinking", reasoningText)
|
thinkingDeltaJSONBytes, _ = sjson.SetBytes(thinkingDeltaJSONBytes, "index", param.ThinkingContentBlockIndex)
|
||||||
results = append(results, "event: content_block_delta\ndata: "+thinkingDeltaJSON+"\n\n")
|
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++
|
param.NextContentBlockIndex++
|
||||||
}
|
}
|
||||||
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}`
|
||||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", param.TextContentBlockIndex)
|
contentBlockStartJSONBytes := []byte(contentBlockStartJSON)
|
||||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", param.TextContentBlockIndex)
|
||||||
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2))
|
||||||
param.TextContentBlockStarted = true
|
param.TextContentBlockStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
contentDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":""}}`
|
||||||
contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "index", param.TextContentBlockIndex)
|
contentDeltaJSONBytes := []byte(contentDeltaJSON)
|
||||||
contentDeltaJSON, _ = sjson.Set(contentDeltaJSON, "delta.text", content.String())
|
contentDeltaJSONBytes, _ = sjson.SetBytes(contentDeltaJSONBytes, "index", param.TextContentBlockIndex)
|
||||||
results = append(results, "event: content_block_delta\ndata: "+contentDeltaJSON+"\n\n")
|
contentDeltaJSONBytes, _ = sjson.SetBytes(contentDeltaJSONBytes, "delta.text", content.String())
|
||||||
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", contentDeltaJSONBytes, 2))
|
||||||
|
|
||||||
// Accumulate content
|
// Accumulate content
|
||||||
param.ContentAccumulator.WriteString(content.String())
|
param.ContentAccumulator.WriteString(content.String())
|
||||||
@@ -242,10 +245,11 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
|
|
||||||
// Send content_block_start for tool_use
|
// Send content_block_start for tool_use
|
||||||
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
contentBlockStartJSON := `{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"","name":"","input":{}}}`
|
||||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "index", blockIndex)
|
contentBlockStartJSONBytes := []byte(contentBlockStartJSON)
|
||||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.id", util.SanitizeClaudeToolID(accumulator.ID))
|
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "index", blockIndex)
|
||||||
contentBlockStartJSON, _ = sjson.Set(contentBlockStartJSON, "content_block.name", accumulator.Name)
|
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "content_block.id", util.SanitizeClaudeToolID(accumulator.ID))
|
||||||
results = append(results, "event: content_block_start\ndata: "+contentBlockStartJSON+"\n\n")
|
contentBlockStartJSONBytes, _ = sjson.SetBytes(contentBlockStartJSONBytes, "content_block.name", accumulator.Name)
|
||||||
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_start", contentBlockStartJSONBytes, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle function arguments
|
// Handle function arguments
|
||||||
@@ -273,9 +277,9 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
|
|
||||||
// Send content_block_stop for thinking content if needed
|
// Send content_block_stop for thinking content if needed
|
||||||
if param.ThinkingContentBlockStarted {
|
if param.ThinkingContentBlockStarted {
|
||||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||||
param.ThinkingContentBlockStarted = false
|
param.ThinkingContentBlockStarted = false
|
||||||
param.ThinkingContentBlockIndex = -1
|
param.ThinkingContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -291,15 +295,15 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
|
|
||||||
// Send complete input_json_delta with all accumulated arguments
|
// Send complete input_json_delta with all accumulated arguments
|
||||||
if accumulator.Arguments.Len() > 0 {
|
if accumulator.Arguments.Len() > 0 {
|
||||||
inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
inputDeltaJSON := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
|
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "index", blockIndex)
|
||||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||||
results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", inputDeltaJSON, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
|
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", blockIndex)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||||
delete(param.ToolCallBlockIndexes, index)
|
delete(param.ToolCallBlockIndexes, index)
|
||||||
}
|
}
|
||||||
param.ContentBlocksStopped = true
|
param.ContentBlocksStopped = true
|
||||||
@@ -316,14 +320,14 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
if usage.Exists() && usage.Type != gjson.Null {
|
if usage.Exists() && usage.Type != gjson.Null {
|
||||||
inputTokens, outputTokens, cachedTokens = extractOpenAIUsage(usage)
|
inputTokens, outputTokens, cachedTokens = extractOpenAIUsage(usage)
|
||||||
// Send message_delta with usage
|
// Send message_delta with usage
|
||||||
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
messageDeltaJSON := []byte(`{"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.SetBytes(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.input_tokens", inputTokens)
|
||||||
messageDeltaJSON, _ = sjson.Set(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
messageDeltaJSON, _ = sjson.SetBytes(messageDeltaJSON, "usage.output_tokens", outputTokens)
|
||||||
if cachedTokens > 0 {
|
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
|
param.MessageDeltaSent = true
|
||||||
|
|
||||||
emitMessageStopIfNeeded(param, &results)
|
emitMessageStopIfNeeded(param, &results)
|
||||||
@@ -334,14 +338,14 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertOpenAIDoneToAnthropic handles the [DONE] marker and sends final events
|
// convertOpenAIDoneToAnthropic handles the [DONE] marker and sends final events
|
||||||
func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) []string {
|
func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams) [][]byte {
|
||||||
var results []string
|
var results [][]byte
|
||||||
|
|
||||||
// Ensure all content blocks are stopped before final events
|
// Ensure all content blocks are stopped before final events
|
||||||
if param.ThinkingContentBlockStarted {
|
if param.ThinkingContentBlockStarted {
|
||||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||||
param.ThinkingContentBlockStarted = false
|
param.ThinkingContentBlockStarted = false
|
||||||
param.ThinkingContentBlockIndex = -1
|
param.ThinkingContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -354,15 +358,15 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
blockIndex := param.toolContentBlockIndex(index)
|
blockIndex := param.toolContentBlockIndex(index)
|
||||||
|
|
||||||
if accumulator.Arguments.Len() > 0 {
|
if accumulator.Arguments.Len() > 0 {
|
||||||
inputDeltaJSON := `{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`
|
inputDeltaJSON := []byte(`{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}}`)
|
||||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "index", blockIndex)
|
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "index", blockIndex)
|
||||||
inputDeltaJSON, _ = sjson.Set(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
inputDeltaJSON, _ = sjson.SetBytes(inputDeltaJSON, "delta.partial_json", util.FixJSON(accumulator.Arguments.String()))
|
||||||
results = append(results, "event: content_block_delta\ndata: "+inputDeltaJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_delta", inputDeltaJSON, 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", blockIndex)
|
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", blockIndex)
|
||||||
results = append(results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||||
delete(param.ToolCallBlockIndexes, index)
|
delete(param.ToolCallBlockIndexes, index)
|
||||||
}
|
}
|
||||||
param.ContentBlocksStopped = true
|
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 we haven't sent message_delta yet (no usage info was received), send it now
|
||||||
if param.FinishReason != "" && !param.MessageDeltaSent {
|
if param.FinishReason != "" && !param.MessageDeltaSent {
|
||||||
messageDeltaJSON := `{"type":"message_delta","delta":{"stop_reason":"","stop_sequence":null},"usage":{"input_tokens":0,"output_tokens":0}}`
|
messageDeltaJSON := []byte(`{"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.SetBytes(messageDeltaJSON, "delta.stop_reason", mapOpenAIFinishReasonToAnthropic(effectiveOpenAIFinishReason(param)))
|
||||||
results = append(results, "event: message_delta\ndata: "+messageDeltaJSON+"\n\n")
|
results = append(results, translatorcommon.AppendSSEEventBytes(nil, "message_delta", messageDeltaJSON, 2))
|
||||||
param.MessageDeltaSent = true
|
param.MessageDeltaSent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,12 +386,12 @@ func convertOpenAIDoneToAnthropic(param *ConvertOpenAIResponseToAnthropicParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convertOpenAINonStreamingToAnthropic converts OpenAI non-streaming response to Anthropic format
|
// convertOpenAINonStreamingToAnthropic converts OpenAI non-streaming response to Anthropic format
|
||||||
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
|
func convertOpenAINonStreamingToAnthropic(rawJSON []byte) [][]byte {
|
||||||
root := gjson.ParseBytes(rawJSON)
|
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 := []byte(`{"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.SetBytes(out, "id", root.Get("id").String())
|
||||||
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
out, _ = sjson.SetBytes(out, "model", root.Get("model").String())
|
||||||
|
|
||||||
// Process message content and tool calls
|
// Process message content and tool calls
|
||||||
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() && len(choices.Array()) > 0 {
|
||||||
@@ -398,59 +402,59 @@ func convertOpenAINonStreamingToAnthropic(rawJSON []byte) []string {
|
|||||||
if reasoningText == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", reasoningText)
|
block, _ = sjson.SetBytes(block, "thinking", reasoningText)
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle text content
|
// Handle text content
|
||||||
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
|
if content := choice.Get("message.content"); content.Exists() && content.String() != "" {
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", content.String())
|
block, _ = sjson.SetBytes(block, "text", content.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tool calls
|
// Handle tool calls
|
||||||
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
if toolCalls := choice.Get("message.tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||||
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUseBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
||||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", toolCall.Get("function.name").String())
|
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "name", toolCall.Get("function.name").String())
|
||||||
|
|
||||||
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsJSON := gjson.Parse(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if argsJSON.IsObject() {
|
if argsJSON.IsObject() {
|
||||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
|
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(argsJSON.Raw))
|
||||||
} else {
|
} else {
|
||||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`))
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set stop reason
|
// Set stop reason
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
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
|
// Set usage information
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(usage)
|
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(usage)
|
||||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||||
if cachedTokens > 0 {
|
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
|
// mapOpenAIFinishReasonToAnthropic maps OpenAI finish reasons to Anthropic equivalents
|
||||||
@@ -513,32 +517,32 @@ func collectOpenAIReasoningTexts(node gjson.Result) []string {
|
|||||||
return texts
|
return texts
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) {
|
func stopThinkingContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) {
|
||||||
if !param.ThinkingContentBlockStarted {
|
if !param.ThinkingContentBlockStarted {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.ThinkingContentBlockIndex)
|
||||||
*results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
*results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||||
param.ThinkingContentBlockStarted = false
|
param.ThinkingContentBlockStarted = false
|
||||||
param.ThinkingContentBlockIndex = -1
|
param.ThinkingContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func emitMessageStopIfNeeded(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) {
|
func emitMessageStopIfNeeded(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) {
|
||||||
if param.MessageStopSent {
|
if param.MessageStopSent {
|
||||||
return
|
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
|
param.MessageStopSent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[]string) {
|
func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results *[][]byte) {
|
||||||
if !param.TextContentBlockStarted {
|
if !param.TextContentBlockStarted {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentBlockStopJSON := `{"type":"content_block_stop","index":0}`
|
contentBlockStopJSON := []byte(`{"type":"content_block_stop","index":0}`)
|
||||||
contentBlockStopJSON, _ = sjson.Set(contentBlockStopJSON, "index", param.TextContentBlockIndex)
|
contentBlockStopJSON, _ = sjson.SetBytes(contentBlockStopJSON, "index", param.TextContentBlockIndex)
|
||||||
*results = append(*results, "event: content_block_stop\ndata: "+contentBlockStopJSON+"\n\n")
|
*results = append(*results, translatorcommon.AppendSSEEventBytes(nil, "content_block_stop", contentBlockStopJSON, 2))
|
||||||
param.TextContentBlockStarted = false
|
param.TextContentBlockStarted = false
|
||||||
param.TextContentBlockIndex = -1
|
param.TextContentBlockIndex = -1
|
||||||
}
|
}
|
||||||
@@ -552,15 +556,15 @@ func stopTextContentBlock(param *ConvertOpenAIResponseToAnthropicParams, results
|
|||||||
// - param: A pointer to a parameter object for the conversion.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An Anthropic-compatible JSON response.
|
// - []byte: An Anthropic-compatible JSON response.
|
||||||
func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
_ = requestRawJSON
|
_ = requestRawJSON
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
toolNameMap := util.ToolNameMapFromClaudeRequest(originalRequestRawJSON)
|
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 := []byte(`{"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.SetBytes(out, "id", root.Get("id").String())
|
||||||
out, _ = sjson.Set(out, "model", root.Get("model").String())
|
out, _ = sjson.SetBytes(out, "model", root.Get("model").String())
|
||||||
|
|
||||||
hasToolCall := false
|
hasToolCall := false
|
||||||
stopReasonSet := false
|
stopReasonSet := false
|
||||||
@@ -569,7 +573,7 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
choice := choices.Array()[0]
|
choice := choices.Array()[0]
|
||||||
|
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
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
|
stopReasonSet = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,9 +587,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if textBuilder.Len() == 0 {
|
if textBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", textBuilder.String())
|
block, _ = sjson.SetBytes(block, "text", textBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
textBuilder.Reset()
|
textBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,9 +597,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if thinkingBuilder.Len() == 0 {
|
if thinkingBuilder.Len() == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", thinkingBuilder.String())
|
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
thinkingBuilder.Reset()
|
thinkingBuilder.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,23 +615,23 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if toolCalls.IsArray() {
|
if toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
toolCalls.ForEach(func(_, tc gjson.Result) bool {
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUse := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUse := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolUse, _ = sjson.Set(toolUse, "id", util.SanitizeClaudeToolID(tc.Get("id").String()))
|
toolUse, _ = sjson.SetBytes(toolUse, "id", util.SanitizeClaudeToolID(tc.Get("id").String()))
|
||||||
toolUse, _ = sjson.Set(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String()))
|
toolUse, _ = sjson.SetBytes(toolUse, "name", util.MapToolName(toolNameMap, tc.Get("function.name").String()))
|
||||||
|
|
||||||
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
argsStr := util.FixJSON(tc.Get("function.arguments").String())
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsJSON := gjson.Parse(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if argsJSON.IsObject() {
|
if argsJSON.IsObject() {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", argsJSON.Raw)
|
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(argsJSON.Raw))
|
||||||
} else {
|
} else {
|
||||||
toolUse, _ = sjson.SetRaw(toolUse, "input", "{}")
|
toolUse, _ = sjson.SetRawBytes(toolUse, "input", []byte(`{}`))
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -647,9 +651,9 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
} else if contentResult.Type == gjson.String {
|
} else if contentResult.Type == gjson.String {
|
||||||
textContent := contentResult.String()
|
textContent := contentResult.String()
|
||||||
if textContent != "" {
|
if textContent != "" {
|
||||||
block := `{"type":"text","text":""}`
|
block := []byte(`{"type":"text","text":""}`)
|
||||||
block, _ = sjson.Set(block, "text", textContent)
|
block, _ = sjson.SetBytes(block, "text", textContent)
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -659,32 +663,32 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
if reasoningText == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
block := `{"type":"thinking","thinking":""}`
|
block := []byte(`{"type":"thinking","thinking":""}`)
|
||||||
block, _ = sjson.Set(block, "thinking", reasoningText)
|
block, _ = sjson.SetBytes(block, "thinking", reasoningText)
|
||||||
out, _ = sjson.SetRaw(out, "content.-1", block)
|
out, _ = sjson.SetRawBytes(out, "content.-1", block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
if toolCalls := message.Get("tool_calls"); toolCalls.Exists() && toolCalls.IsArray() {
|
||||||
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
toolCalls.ForEach(func(_, toolCall gjson.Result) bool {
|
||||||
hasToolCall = true
|
hasToolCall = true
|
||||||
toolUseBlock := `{"type":"tool_use","id":"","name":"","input":{}}`
|
toolUseBlock := []byte(`{"type":"tool_use","id":"","name":"","input":{}}`)
|
||||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "id", util.SanitizeClaudeToolID(toolCall.Get("id").String()))
|
||||||
toolUseBlock, _ = sjson.Set(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String()))
|
toolUseBlock, _ = sjson.SetBytes(toolUseBlock, "name", util.MapToolName(toolNameMap, toolCall.Get("function.name").String()))
|
||||||
|
|
||||||
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
argsStr := util.FixJSON(toolCall.Get("function.arguments").String())
|
||||||
if argsStr != "" && gjson.Valid(argsStr) {
|
if argsStr != "" && gjson.Valid(argsStr) {
|
||||||
argsJSON := gjson.Parse(argsStr)
|
argsJSON := gjson.Parse(argsStr)
|
||||||
if argsJSON.IsObject() {
|
if argsJSON.IsObject() {
|
||||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", argsJSON.Raw)
|
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(argsJSON.Raw))
|
||||||
} else {
|
} else {
|
||||||
toolUseBlock, _ = sjson.SetRaw(toolUseBlock, "input", "{}")
|
toolUseBlock, _ = sjson.SetRawBytes(toolUseBlock, "input", []byte(`{}`))
|
||||||
}
|
}
|
||||||
} else {
|
} 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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -693,26 +697,26 @@ func ConvertOpenAIResponseToClaudeNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
if respUsage := root.Get("usage"); respUsage.Exists() {
|
if respUsage := root.Get("usage"); respUsage.Exists() {
|
||||||
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(respUsage)
|
inputTokens, outputTokens, cachedTokens := extractOpenAIUsage(respUsage)
|
||||||
out, _ = sjson.Set(out, "usage.input_tokens", inputTokens)
|
out, _ = sjson.SetBytes(out, "usage.input_tokens", inputTokens)
|
||||||
out, _ = sjson.Set(out, "usage.output_tokens", outputTokens)
|
out, _ = sjson.SetBytes(out, "usage.output_tokens", outputTokens)
|
||||||
if cachedTokens > 0 {
|
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 !stopReasonSet {
|
||||||
if hasToolCall {
|
if hasToolCall {
|
||||||
out, _ = sjson.Set(out, "stop_reason", "tool_use")
|
out, _ = sjson.SetBytes(out, "stop_reason", "tool_use")
|
||||||
} else {
|
} else {
|
||||||
out, _ = sjson.Set(out, "stop_reason", "end_turn")
|
out, _ = sjson.SetBytes(out, "stop_reason", "end_turn")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClaudeTokenCount(ctx context.Context, count int64) string {
|
func ClaudeTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"input_tokens":%d}`, count)
|
return translatorcommon.ClaudeInputTokensJSON(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64) {
|
func extractOpenAIUsage(usage gjson.Result) (int64, int64, int64) {
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ package geminiCLI
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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/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.
|
// 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.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response.
|
// - [][]byte: A slice of Gemini-compatible JSON responses.
|
||||||
func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
outputs := ConvertOpenAIResponseToGemini(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
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++ {
|
for i := 0; i < len(outputs); i++ {
|
||||||
json := `{"response": {}}`
|
newOutputs = append(newOutputs, translatorcommon.WrapGeminiCLIResponse(outputs[i]))
|
||||||
output, _ := sjson.SetRaw(json, "response", outputs[i])
|
|
||||||
newOutputs = append(newOutputs, output)
|
|
||||||
}
|
}
|
||||||
return newOutputs
|
return newOutputs
|
||||||
}
|
}
|
||||||
@@ -45,14 +42,12 @@ func ConvertOpenAIResponseToGeminiCLI(ctx context.Context, modelName string, ori
|
|||||||
// - param: A pointer to a parameter object for the conversion.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response.
|
// - []byte: A Gemini-compatible JSON response.
|
||||||
func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
func ConvertOpenAIResponseToGeminiCLINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||||
strJSON := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
out := ConvertOpenAIResponseToGeminiNonStream(ctx, modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
json := `{"response": {}}`
|
return translatorcommon.WrapGeminiCLIResponse(out)
|
||||||
strJSON, _ = sjson.SetRaw(json, "response", strJSON)
|
|
||||||
return strJSON
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiCLITokenCount(ctx context.Context, count int64) string {
|
func GeminiCLITokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import (
|
|||||||
func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
// Base OpenAI Chat Completions API template
|
// Base OpenAI Chat Completions API template
|
||||||
out := `{"model":"","messages":[]}`
|
out := []byte(`{"model":"","messages":[]}`)
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
@@ -39,29 +39,29 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Model mapping
|
// Model mapping
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// Generation config mapping
|
// Generation config mapping
|
||||||
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
if genConfig := root.Get("generationConfig"); genConfig.Exists() {
|
||||||
// Temperature
|
// Temperature
|
||||||
if temp := genConfig.Get("temperature"); temp.Exists() {
|
if temp := genConfig.Get("temperature"); temp.Exists() {
|
||||||
out, _ = sjson.Set(out, "temperature", temp.Float())
|
out, _ = sjson.SetBytes(out, "temperature", temp.Float())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max tokens
|
// Max tokens
|
||||||
if maxTokens := genConfig.Get("maxOutputTokens"); maxTokens.Exists() {
|
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
|
// Top P
|
||||||
if topP := genConfig.Get("topP"); topP.Exists() {
|
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)
|
// Top K (OpenAI doesn't have direct equivalent, but we can map it)
|
||||||
if topK := genConfig.Get("topK"); topK.Exists() {
|
if topK := genConfig.Get("topK"); topK.Exists() {
|
||||||
// Store as custom parameter for potential use
|
// 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
|
// Stop sequences
|
||||||
@@ -72,13 +72,13 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
if len(stops) > 0 {
|
if len(stops) > 0 {
|
||||||
out, _ = sjson.Set(out, "stop", stops)
|
out, _ = sjson.SetBytes(out, "stop", stops)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Candidate count (OpenAI 'n' parameter)
|
// Candidate count (OpenAI 'n' parameter)
|
||||||
if candidateCount := genConfig.Get("candidateCount"); candidateCount.Exists() {
|
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.
|
// Map Gemini thinkingConfig to OpenAI reasoning_effort.
|
||||||
@@ -92,7 +92,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
if thinkingLevel.Exists() {
|
if thinkingLevel.Exists() {
|
||||||
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
effort := strings.ToLower(strings.TrimSpace(thinkingLevel.String()))
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
thinkingBudget := thinkingConfig.Get("thinkingBudget")
|
thinkingBudget := thinkingConfig.Get("thinkingBudget")
|
||||||
@@ -101,7 +101,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
if thinkingBudget.Exists() {
|
if thinkingBudget.Exists() {
|
||||||
if effort, ok := thinking.ConvertBudgetToLevel(int(thinkingBudget.Int())); ok {
|
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
|
// Stream parameter
|
||||||
out, _ = sjson.Set(out, "stream", stream)
|
out, _ = sjson.SetBytes(out, "stream", stream)
|
||||||
|
|
||||||
// Process contents (Gemini messages) -> OpenAI messages
|
// Process contents (Gemini messages) -> OpenAI messages
|
||||||
var toolCallIDs []string // Track tool call IDs for matching with tool results
|
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() {
|
if systemInstruction.Exists() {
|
||||||
parts := systemInstruction.Get("parts")
|
parts := systemInstruction.Get("parts")
|
||||||
msg := `{"role":"system","content":[]}`
|
msg := []byte(`{"role":"system","content":[]}`)
|
||||||
hasContent := false
|
hasContent := false
|
||||||
|
|
||||||
if parts.Exists() && parts.IsArray() {
|
if parts.Exists() && parts.IsArray() {
|
||||||
parts.ForEach(func(_, part gjson.Result) bool {
|
parts.ForEach(func(_, part gjson.Result) bool {
|
||||||
// Handle text parts
|
// Handle text parts
|
||||||
if text := part.Get("text"); text.Exists() {
|
if text := part.Get("text"); text.Exists() {
|
||||||
contentPart := `{"type":"text","text":""}`
|
contentPart := []byte(`{"type":"text","text":""}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "text", text.String())
|
contentPart, _ = sjson.SetBytes(contentPart, "text", text.String())
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", contentPart)
|
||||||
hasContent = true
|
hasContent = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,9 +144,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
data := inlineData.Get("data").String()
|
data := inlineData.Get("data").String()
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
|
|
||||||
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL)
|
||||||
msg, _ = sjson.SetRaw(msg, "content.-1", contentPart)
|
msg, _ = sjson.SetRawBytes(msg, "content.-1", contentPart)
|
||||||
hasContent = true
|
hasContent = true
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -154,7 +154,7 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
}
|
}
|
||||||
|
|
||||||
if hasContent {
|
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"
|
role = "assistant"
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := `{"role":"","content":""}`
|
msg := []byte(`{"role":"","content":""}`)
|
||||||
msg, _ = sjson.Set(msg, "role", role)
|
msg, _ = sjson.SetBytes(msg, "role", role)
|
||||||
|
|
||||||
var textBuilder strings.Builder
|
var textBuilder strings.Builder
|
||||||
contentWrapper := `{"arr":[]}`
|
contentWrapper := []byte(`{"arr":[]}`)
|
||||||
contentPartsCount := 0
|
contentPartsCount := 0
|
||||||
onlyTextContent := true
|
onlyTextContent := true
|
||||||
toolCallsWrapper := `{"arr":[]}`
|
toolCallsWrapper := []byte(`{"arr":[]}`)
|
||||||
toolCallsCount := 0
|
toolCallsCount := 0
|
||||||
|
|
||||||
if parts.Exists() && parts.IsArray() {
|
if parts.Exists() && parts.IsArray() {
|
||||||
@@ -184,9 +184,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
if text := part.Get("text"); text.Exists() {
|
if text := part.Get("text"); text.Exists() {
|
||||||
formattedText := text.String()
|
formattedText := text.String()
|
||||||
textBuilder.WriteString(formattedText)
|
textBuilder.WriteString(formattedText)
|
||||||
contentPart := `{"type":"text","text":""}`
|
contentPart := []byte(`{"type":"text","text":""}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "text", formattedText)
|
contentPart, _ = sjson.SetBytes(contentPart, "text", formattedText)
|
||||||
contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
|
contentWrapper, _ = sjson.SetRawBytes(contentWrapper, "arr.-1", contentPart)
|
||||||
contentPartsCount++
|
contentPartsCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,9 +201,9 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
data := inlineData.Get("data").String()
|
data := inlineData.Get("data").String()
|
||||||
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
imageURL := fmt.Sprintf("data:%s;base64,%s", mimeType, data)
|
||||||
|
|
||||||
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL)
|
||||||
contentWrapper, _ = sjson.SetRaw(contentWrapper, "arr.-1", contentPart)
|
contentWrapper, _ = sjson.SetRawBytes(contentWrapper, "arr.-1", contentPart)
|
||||||
contentPartsCount++
|
contentPartsCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,32 +212,32 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
toolCallID := genToolCallID()
|
toolCallID := genToolCallID()
|
||||||
toolCallIDs = append(toolCallIDs, toolCallID)
|
toolCallIDs = append(toolCallIDs, toolCallID)
|
||||||
|
|
||||||
toolCall := `{"id":"","type":"function","function":{"name":"","arguments":""}}`
|
toolCall := []byte(`{"id":"","type":"function","function":{"name":"","arguments":""}}`)
|
||||||
toolCall, _ = sjson.Set(toolCall, "id", toolCallID)
|
toolCall, _ = sjson.SetBytes(toolCall, "id", toolCallID)
|
||||||
toolCall, _ = sjson.Set(toolCall, "function.name", functionCall.Get("name").String())
|
toolCall, _ = sjson.SetBytes(toolCall, "function.name", functionCall.Get("name").String())
|
||||||
|
|
||||||
// Convert args to arguments JSON string
|
// Convert args to arguments JSON string
|
||||||
if args := functionCall.Get("args"); args.Exists() {
|
if args := functionCall.Get("args"); args.Exists() {
|
||||||
toolCall, _ = sjson.Set(toolCall, "function.arguments", args.Raw)
|
toolCall, _ = sjson.SetBytes(toolCall, "function.arguments", args.Raw)
|
||||||
} else {
|
} 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++
|
toolCallsCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle function responses (Gemini) -> tool role messages (OpenAI)
|
// Handle function responses (Gemini) -> tool role messages (OpenAI)
|
||||||
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
|
if functionResponse := part.Get("functionResponse"); functionResponse.Exists() {
|
||||||
// Create tool message for function response
|
// 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
|
// Convert response.content to JSON string
|
||||||
if response := functionResponse.Get("response"); response.Exists() {
|
if response := functionResponse.Get("response"); response.Exists() {
|
||||||
if contentField := response.Get("content"); contentField.Exists() {
|
if contentField := response.Get("content"); contentField.Exists() {
|
||||||
toolMsg, _ = sjson.Set(toolMsg, "content", contentField.Raw)
|
toolMsg, _ = sjson.SetBytes(toolMsg, "content", contentField.Raw)
|
||||||
} else {
|
} 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 {
|
if len(toolCallIDs) > 0 {
|
||||||
// Use the last tool call ID (simple matching by function name)
|
// Use the last tool call ID (simple matching by function name)
|
||||||
// In a real implementation, you might want more sophisticated matching
|
// 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 {
|
} else {
|
||||||
// Generate a tool call ID if none available
|
// 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
|
return true
|
||||||
@@ -262,18 +262,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
// Set content
|
// Set content
|
||||||
if contentPartsCount > 0 {
|
if contentPartsCount > 0 {
|
||||||
if onlyTextContent {
|
if onlyTextContent {
|
||||||
msg, _ = sjson.Set(msg, "content", textBuilder.String())
|
msg, _ = sjson.SetBytes(msg, "content", textBuilder.String())
|
||||||
} else {
|
} 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
|
// Set tool calls if any
|
||||||
if toolCallsCount > 0 {
|
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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -283,18 +283,18 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
tools.ForEach(func(_, tool gjson.Result) bool {
|
tools.ForEach(func(_, tool gjson.Result) bool {
|
||||||
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
|
if functionDeclarations := tool.Get("functionDeclarations"); functionDeclarations.Exists() && functionDeclarations.IsArray() {
|
||||||
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
|
functionDeclarations.ForEach(func(_, funcDecl gjson.Result) bool {
|
||||||
openAITool := `{"type":"function","function":{"name":"","description":""}}`
|
openAITool := []byte(`{"type":"function","function":{"name":"","description":""}}`)
|
||||||
openAITool, _ = sjson.Set(openAITool, "function.name", funcDecl.Get("name").String())
|
openAITool, _ = sjson.SetBytes(openAITool, "function.name", funcDecl.Get("name").String())
|
||||||
openAITool, _ = sjson.Set(openAITool, "function.description", funcDecl.Get("description").String())
|
openAITool, _ = sjson.SetBytes(openAITool, "function.description", funcDecl.Get("description").String())
|
||||||
|
|
||||||
// Convert parameters schema
|
// Convert parameters schema
|
||||||
if parameters := funcDecl.Get("parameters"); parameters.Exists() {
|
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() {
|
} 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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -308,14 +308,14 @@ func ConvertGeminiRequestToOpenAI(modelName string, inputRawJSON []byte, stream
|
|||||||
mode := functionCallingConfig.Get("mode").String()
|
mode := functionCallingConfig.Get("mode").String()
|
||||||
switch mode {
|
switch mode {
|
||||||
case "NONE":
|
case "NONE":
|
||||||
out, _ = sjson.Set(out, "tool_choice", "none")
|
out, _ = sjson.SetBytes(out, "tool_choice", "none")
|
||||||
case "AUTO":
|
case "AUTO":
|
||||||
out, _ = sjson.Set(out, "tool_choice", "auto")
|
out, _ = sjson.SetBytes(out, "tool_choice", "auto")
|
||||||
case "ANY":
|
case "ANY":
|
||||||
out, _ = sjson.Set(out, "tool_choice", "required")
|
out, _ = sjson.SetBytes(out, "tool_choice", "required")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(out)
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -44,8 +45,8 @@ type ToolCallAccumulator struct {
|
|||||||
// - param: A pointer to a parameter object for the conversion.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing a Gemini-compatible JSON response.
|
// - [][]byte: A slice of Gemini-compatible JSON responses.
|
||||||
func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if *param == nil {
|
if *param == nil {
|
||||||
*param = &ConvertOpenAIResponseToGeminiParams{
|
*param = &ConvertOpenAIResponseToGeminiParams{
|
||||||
ToolCallsAccumulator: nil,
|
ToolCallsAccumulator: nil,
|
||||||
@@ -55,8 +56,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle [DONE] marker
|
// Handle [DONE] marker
|
||||||
if strings.TrimSpace(string(rawJSON)) == "[DONE]" {
|
if bytes.Equal(bytes.TrimSpace(rawJSON), []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
@@ -76,51 +77,51 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
if len(choices.Array()) == 0 {
|
if len(choices.Array()) == 0 {
|
||||||
// This is a usage-only chunk, handle usage and return
|
// This is a usage-only chunk, handle usage and return
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
template := `{"candidates":[],"usageMetadata":{}}`
|
template := []byte(`{"candidates":[],"usageMetadata":{}}`)
|
||||||
|
|
||||||
// Set model if available
|
// Set model if available
|
||||||
if model := root.Get("model"); model.Exists() {
|
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.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
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 {
|
choices.ForEach(func(choiceIndex, choice gjson.Result) bool {
|
||||||
// Base Gemini response template without finishReason; set when known
|
// 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
|
// Set model if available
|
||||||
if model := root.Get("model"); model.Exists() {
|
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
|
_ = int(choice.Get("index").Int()) // choiceIdx not used in streaming
|
||||||
delta := choice.Get("delta")
|
delta := choice.Get("delta")
|
||||||
baseTemplate := template
|
baseTemplate := append([]byte(nil), template...)
|
||||||
|
|
||||||
// Handle role (only in first chunk)
|
// Handle role (only in first chunk)
|
||||||
if role := delta.Get("role"); role.Exists() && (*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk {
|
if role := delta.Get("role"); role.Exists() && (*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk {
|
||||||
// OpenAI assistant -> Gemini model
|
// OpenAI assistant -> Gemini model
|
||||||
if role.String() == "assistant" {
|
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
|
(*param).(*ConvertOpenAIResponseToGeminiParams).IsFirstChunk = false
|
||||||
results = append(results, template)
|
results = append(results, template)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunkOutputs []string
|
var chunkOutputs [][]byte
|
||||||
|
|
||||||
// Handle reasoning/thinking delta
|
// Handle reasoning/thinking delta
|
||||||
if reasoning := delta.Get("reasoning_content"); reasoning.Exists() {
|
if reasoning := delta.Get("reasoning_content"); reasoning.Exists() {
|
||||||
@@ -128,9 +129,9 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
if reasoningText == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reasoningTemplate := baseTemplate
|
reasoningTemplate := append([]byte(nil), baseTemplate...)
|
||||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
|
reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.thought", true)
|
||||||
reasoningTemplate, _ = sjson.Set(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
|
reasoningTemplate, _ = sjson.SetBytes(reasoningTemplate, "candidates.0.content.parts.0.text", reasoningText)
|
||||||
chunkOutputs = append(chunkOutputs, reasoningTemplate)
|
chunkOutputs = append(chunkOutputs, reasoningTemplate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,8 +142,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
|
(*param).(*ConvertOpenAIResponseToGeminiParams).ContentAccumulator.WriteString(contentText)
|
||||||
|
|
||||||
// Create text part for this delta
|
// Create text part for this delta
|
||||||
contentTemplate := baseTemplate
|
contentTemplate := append([]byte(nil), baseTemplate...)
|
||||||
contentTemplate, _ = sjson.Set(contentTemplate, "candidates.0.content.parts.0.text", contentText)
|
contentTemplate, _ = sjson.SetBytes(contentTemplate, "candidates.0.content.parts.0.text", contentText)
|
||||||
chunkOutputs = append(chunkOutputs, contentTemplate)
|
chunkOutputs = append(chunkOutputs, contentTemplate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +208,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
// Handle finish reason
|
// Handle finish reason
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
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 we have accumulated tool calls, output them now
|
||||||
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
|
if len((*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator) > 0 {
|
||||||
@@ -215,8 +216,8 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
|
for _, accumulator := range (*param).(*ConvertOpenAIResponseToGeminiParams).ToolCallsAccumulator {
|
||||||
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
||||||
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
||||||
template, _ = sjson.Set(template, namePath, accumulator.Name)
|
template, _ = sjson.SetBytes(template, namePath, accumulator.Name)
|
||||||
template, _ = sjson.SetRaw(template, argsPath, parseArgsToObjectRaw(accumulator.Arguments.String()))
|
template, _ = sjson.SetRawBytes(template, argsPath, []byte(parseArgsToObjectRaw(accumulator.Arguments.String())))
|
||||||
partIndex++
|
partIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,11 +231,11 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
|
|
||||||
// Handle usage information
|
// Handle usage information
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
template, _ = sjson.Set(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
template, _ = sjson.SetBytes(template, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||||
template, _ = sjson.Set(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
template, _ = sjson.SetBytes(template, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||||
template, _ = sjson.Set(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
template, _ = sjson.SetBytes(template, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||||
template, _ = sjson.Set(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
template, _ = sjson.SetBytes(template, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||||
}
|
}
|
||||||
results = append(results, template)
|
results = append(results, template)
|
||||||
return true
|
return true
|
||||||
@@ -244,7 +245,7 @@ func ConvertOpenAIResponseToGemini(_ context.Context, _ string, originalRequestR
|
|||||||
})
|
})
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// mapOpenAIFinishReasonToGemini maps OpenAI finish reasons to Gemini finish reasons
|
// mapOpenAIFinishReasonToGemini maps OpenAI finish reasons to Gemini finish reasons
|
||||||
@@ -310,7 +311,7 @@ func tolerantParseJSONObjectRaw(s string) string {
|
|||||||
runes := []rune(content)
|
runes := []rune(content)
|
||||||
n := len(runes)
|
n := len(runes)
|
||||||
i := 0
|
i := 0
|
||||||
result := "{}"
|
result := []byte(`{}`)
|
||||||
|
|
||||||
for i < n {
|
for i < n {
|
||||||
// Skip whitespace and commas
|
// Skip whitespace and commas
|
||||||
@@ -362,10 +363,10 @@ func tolerantParseJSONObjectRaw(s string) string {
|
|||||||
valToken, ni := parseJSONStringRunes(runes, i)
|
valToken, ni := parseJSONStringRunes(runes, i)
|
||||||
if ni == -1 {
|
if ni == -1 {
|
||||||
// Malformed; treat as empty string
|
// Malformed; treat as empty string
|
||||||
result, _ = sjson.Set(result, sjsonKey, "")
|
result, _ = sjson.SetBytes(result, sjsonKey, "")
|
||||||
i = n
|
i = n
|
||||||
} else {
|
} else {
|
||||||
result, _ = sjson.Set(result, sjsonKey, jsonStringTokenToRawString(valToken))
|
result, _ = sjson.SetBytes(result, sjsonKey, jsonStringTokenToRawString(valToken))
|
||||||
i = ni
|
i = ni
|
||||||
}
|
}
|
||||||
case '{', '[':
|
case '{', '[':
|
||||||
@@ -375,9 +376,9 @@ func tolerantParseJSONObjectRaw(s string) string {
|
|||||||
i = n
|
i = n
|
||||||
} else {
|
} else {
|
||||||
if gjson.Valid(seg) {
|
if gjson.Valid(seg) {
|
||||||
result, _ = sjson.SetRaw(result, sjsonKey, seg)
|
result, _ = sjson.SetRawBytes(result, sjsonKey, []byte(seg))
|
||||||
} else {
|
} else {
|
||||||
result, _ = sjson.Set(result, sjsonKey, seg)
|
result, _ = sjson.SetBytes(result, sjsonKey, seg)
|
||||||
}
|
}
|
||||||
i = ni
|
i = ni
|
||||||
}
|
}
|
||||||
@@ -390,15 +391,15 @@ func tolerantParseJSONObjectRaw(s string) string {
|
|||||||
token := strings.TrimSpace(string(runes[i:j]))
|
token := strings.TrimSpace(string(runes[i:j]))
|
||||||
// Interpret common JSON atoms and numbers; otherwise treat as string
|
// Interpret common JSON atoms and numbers; otherwise treat as string
|
||||||
if token == "true" {
|
if token == "true" {
|
||||||
result, _ = sjson.Set(result, sjsonKey, true)
|
result, _ = sjson.SetBytes(result, sjsonKey, true)
|
||||||
} else if token == "false" {
|
} else if token == "false" {
|
||||||
result, _ = sjson.Set(result, sjsonKey, false)
|
result, _ = sjson.SetBytes(result, sjsonKey, false)
|
||||||
} else if token == "null" {
|
} else if token == "null" {
|
||||||
result, _ = sjson.Set(result, sjsonKey, nil)
|
result, _ = sjson.SetBytes(result, sjsonKey, nil)
|
||||||
} else if numVal, ok := tryParseNumber(token); ok {
|
} else if numVal, ok := tryParseNumber(token); ok {
|
||||||
result, _ = sjson.Set(result, sjsonKey, numVal)
|
result, _ = sjson.SetBytes(result, sjsonKey, numVal)
|
||||||
} else {
|
} else {
|
||||||
result, _ = sjson.Set(result, sjsonKey, token)
|
result, _ = sjson.SetBytes(result, sjsonKey, token)
|
||||||
}
|
}
|
||||||
i = j
|
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.
|
// 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.
|
// - param: A pointer to a parameter object for the conversion.
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: A Gemini-compatible JSON response.
|
// - []byte: A Gemini-compatible JSON response.
|
||||||
func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) string {
|
func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, _ *any) []byte {
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Base Gemini response template without finishReason; set when known
|
// 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
|
// Set model if available
|
||||||
if model := root.Get("model"); model.Exists() {
|
if model := root.Get("model"); model.Exists() {
|
||||||
out, _ = sjson.Set(out, "model", model.String())
|
out, _ = sjson.SetBytes(out, "model", model.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process choices
|
// Process choices
|
||||||
@@ -552,7 +553,7 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
// Set role
|
// Set role
|
||||||
if role := message.Get("role"); role.Exists() {
|
if role := message.Get("role"); role.Exists() {
|
||||||
if role.String() == "assistant" {
|
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 == "" {
|
if reasoningText == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out, _ = sjson.Set(out, fmt.Sprintf("candidates.0.content.parts.%d.thought", partIndex), true)
|
out, _ = sjson.SetBytes(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.text", partIndex), reasoningText)
|
||||||
partIndex++
|
partIndex++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle content first
|
// Handle content first
|
||||||
if content := message.Get("content"); content.Exists() && content.String() != "" {
|
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++
|
partIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,8 +587,8 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
namePath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.name", partIndex)
|
||||||
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
argsPath := fmt.Sprintf("candidates.0.content.parts.%d.functionCall.args", partIndex)
|
||||||
out, _ = sjson.Set(out, namePath, functionName)
|
out, _ = sjson.SetBytes(out, namePath, functionName)
|
||||||
out, _ = sjson.SetRaw(out, argsPath, parseArgsToObjectRaw(functionArgs))
|
out, _ = sjson.SetRawBytes(out, argsPath, []byte(parseArgsToObjectRaw(functionArgs)))
|
||||||
partIndex++
|
partIndex++
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -597,11 +598,11 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
// Handle finish reason
|
// Handle finish reason
|
||||||
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
if finishReason := choice.Get("finish_reason"); finishReason.Exists() {
|
||||||
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
geminiFinishReason := mapOpenAIFinishReasonToGemini(finishReason.String())
|
||||||
out, _ = sjson.Set(out, "candidates.0.finishReason", geminiFinishReason)
|
out, _ = sjson.SetBytes(out, "candidates.0.finishReason", geminiFinishReason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set index
|
// Set index
|
||||||
out, _ = sjson.Set(out, "candidates.0.index", choiceIdx)
|
out, _ = sjson.SetBytes(out, "candidates.0.index", choiceIdx)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
@@ -609,19 +610,19 @@ func ConvertOpenAIResponseToGeminiNonStream(_ context.Context, _ string, origina
|
|||||||
|
|
||||||
// Handle usage information
|
// Handle usage information
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
out, _ = sjson.Set(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
out, _ = sjson.SetBytes(out, "usageMetadata.promptTokenCount", usage.Get("prompt_tokens").Int())
|
||||||
out, _ = sjson.Set(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
out, _ = sjson.SetBytes(out, "usageMetadata.candidatesTokenCount", usage.Get("completion_tokens").Int())
|
||||||
out, _ = sjson.Set(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
out, _ = sjson.SetBytes(out, "usageMetadata.totalTokenCount", usage.Get("total_tokens").Int())
|
||||||
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
if reasoningTokens := reasoningTokensFromUsage(usage); reasoningTokens > 0 {
|
||||||
out, _ = sjson.Set(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
out, _ = sjson.SetBytes(out, "usageMetadata.thoughtsTokenCount", reasoningTokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func GeminiTokenCount(ctx context.Context, count int64) string {
|
func GeminiTokenCount(ctx context.Context, count int64) []byte {
|
||||||
return fmt.Sprintf(`{"totalTokens":%d,"promptTokensDetails":[{"modality":"TEXT","tokenCount":%d}]}`, count, count)
|
return translatorcommon.GeminiTokenCountJSON(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func reasoningTokensFromUsage(usage gjson.Result) int64 {
|
func reasoningTokensFromUsage(usage gjson.Result) int64 {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
// Package openai provides response translation functionality for Gemini CLI to OpenAI API compatibility.
|
// Package chat_completions provides passthrough response translation for OpenAI Chat Completions.
|
||||||
// This package handles the conversion of Gemini CLI API responses into OpenAI Chat Completions-compatible
|
// It normalizes OpenAI-compatible SSE lines by stripping the "data:" prefix and dropping "[DONE]".
|
||||||
// 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
|
package chat_completions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -10,11 +7,9 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConvertOpenAIResponseToOpenAI translates a single chunk of a streaming response from the
|
// ConvertOpenAIResponseToOpenAI normalizes a single chunk of an OpenAI-compatible streaming response.
|
||||||
// Gemini CLI API format to the OpenAI Chat Completions streaming format.
|
// If the chunk is an SSE "data:" line, the prefix is stripped and the remaining JSON payload is returned.
|
||||||
// It processes various Gemini CLI event types and transforms them into OpenAI-compatible JSON responses.
|
// The "[DONE]" marker yields no output.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
// - 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
|
// - param: A pointer to a parameter object for maintaining state between calls
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: A slice of strings, each containing an OpenAI-compatible JSON response
|
// - [][]byte: A slice of JSON payload chunks in OpenAI format.
|
||||||
func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
func ConvertOpenAIResponseToOpenAI(_ context.Context, _ string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) [][]byte {
|
||||||
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
if bytes.HasPrefix(rawJSON, []byte("data:")) {
|
||||||
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
rawJSON = bytes.TrimSpace(rawJSON[5:])
|
||||||
}
|
}
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
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.
|
// ConvertOpenAIResponseToOpenAINonStream passes through 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.
|
|
||||||
//
|
//
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - ctx: The context for the request, used for cancellation and timeout handling
|
// - 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
|
// - param: A pointer to a parameter object for the conversion
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: An OpenAI-compatible JSON response containing all message content and metadata
|
// - []byte: The OpenAI-compatible JSON response.
|
||||||
func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
func ConvertOpenAIResponseToOpenAINonStream(ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []byte {
|
||||||
return string(rawJSON)
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,30 +29,30 @@ import (
|
|||||||
func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte {
|
func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inputRawJSON []byte, stream bool) []byte {
|
||||||
rawJSON := inputRawJSON
|
rawJSON := inputRawJSON
|
||||||
// Base OpenAI chat completions template with default values
|
// Base OpenAI chat completions template with default values
|
||||||
out := `{"model":"","messages":[],"stream":false}`
|
out := []byte(`{"model":"","messages":[],"stream":false}`)
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Set model name
|
// Set model name
|
||||||
out, _ = sjson.Set(out, "model", modelName)
|
out, _ = sjson.SetBytes(out, "model", modelName)
|
||||||
|
|
||||||
// Set stream configuration
|
// 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
|
// Map generation parameters from responses format to chat completions format
|
||||||
if maxTokens := root.Get("max_output_tokens"); maxTokens.Exists() {
|
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() {
|
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
|
// Convert instructions to system message
|
||||||
if instructions := root.Get("instructions"); instructions.Exists() {
|
if instructions := root.Get("instructions"); instructions.Exists() {
|
||||||
systemMessage := `{"role":"system","content":""}`
|
systemMessage := []byte(`{"role":"system","content":""}`)
|
||||||
systemMessage, _ = sjson.Set(systemMessage, "content", instructions.String())
|
systemMessage, _ = sjson.SetBytes(systemMessage, "content", instructions.String())
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", systemMessage)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", systemMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert input array to messages
|
// Convert input array to messages
|
||||||
@@ -70,8 +70,8 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
|||||||
if role == "developer" {
|
if role == "developer" {
|
||||||
role = "user"
|
role = "user"
|
||||||
}
|
}
|
||||||
message := `{"role":"","content":[]}`
|
message := []byte(`{"role":"","content":[]}`)
|
||||||
message, _ = sjson.Set(message, "role", role)
|
message, _ = sjson.SetBytes(message, "role", role)
|
||||||
|
|
||||||
if content := item.Get("content"); content.Exists() && content.IsArray() {
|
if content := item.Get("content"); content.Exists() && content.IsArray() {
|
||||||
var messageContent string
|
var messageContent string
|
||||||
@@ -86,74 +86,74 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
|||||||
switch contentType {
|
switch contentType {
|
||||||
case "input_text", "output_text":
|
case "input_text", "output_text":
|
||||||
text := contentItem.Get("text").String()
|
text := contentItem.Get("text").String()
|
||||||
contentPart := `{"type":"text","text":""}`
|
contentPart := []byte(`{"type":"text","text":""}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "text", text)
|
contentPart, _ = sjson.SetBytes(contentPart, "text", text)
|
||||||
message, _ = sjson.SetRaw(message, "content.-1", contentPart)
|
message, _ = sjson.SetRawBytes(message, "content.-1", contentPart)
|
||||||
case "input_image":
|
case "input_image":
|
||||||
imageURL := contentItem.Get("image_url").String()
|
imageURL := contentItem.Get("image_url").String()
|
||||||
contentPart := `{"type":"image_url","image_url":{"url":""}}`
|
contentPart := []byte(`{"type":"image_url","image_url":{"url":""}}`)
|
||||||
contentPart, _ = sjson.Set(contentPart, "image_url.url", imageURL)
|
contentPart, _ = sjson.SetBytes(contentPart, "image_url.url", imageURL)
|
||||||
message, _ = sjson.SetRaw(message, "content.-1", contentPart)
|
message, _ = sjson.SetRawBytes(message, "content.-1", contentPart)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if messageContent != "" {
|
if messageContent != "" {
|
||||||
message, _ = sjson.Set(message, "content", messageContent)
|
message, _ = sjson.SetBytes(message, "content", messageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(toolCalls) > 0 {
|
if len(toolCalls) > 0 {
|
||||||
message, _ = sjson.Set(message, "tool_calls", toolCalls)
|
message, _ = sjson.SetBytes(message, "tool_calls", toolCalls)
|
||||||
}
|
}
|
||||||
} else if content.Type == gjson.String {
|
} 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":
|
case "function_call":
|
||||||
// Handle function call conversion to assistant message with tool_calls
|
// 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() {
|
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() {
|
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() {
|
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)
|
assistantMessage, _ = sjson.SetRawBytes(assistantMessage, "tool_calls.0", toolCall)
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", assistantMessage)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", assistantMessage)
|
||||||
|
|
||||||
case "function_call_output":
|
case "function_call_output":
|
||||||
// Handle function call output conversion to tool message
|
// 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() {
|
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() {
|
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
|
return true
|
||||||
})
|
})
|
||||||
} else if input.Type == gjson.String {
|
} else if input.Type == gjson.String {
|
||||||
msg := "{}"
|
msg := []byte(`{}`)
|
||||||
msg, _ = sjson.Set(msg, "role", "user")
|
msg, _ = sjson.SetBytes(msg, "role", "user")
|
||||||
msg, _ = sjson.Set(msg, "content", input.String())
|
msg, _ = sjson.SetBytes(msg, "content", input.String())
|
||||||
out, _ = sjson.SetRaw(out, "messages.-1", msg)
|
out, _ = sjson.SetRawBytes(out, "messages.-1", msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert tools from responses format to chat completions format
|
// Convert tools from responses format to chat completions format
|
||||||
@@ -170,45 +170,45 @@ func ConvertOpenAIResponsesRequestToOpenAIChatCompletions(modelName string, inpu
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
chatTool := `{"type":"function","function":{}}`
|
chatTool := []byte(`{"type":"function","function":{}}`)
|
||||||
|
|
||||||
// Convert tool structure from responses format to chat completions format
|
// 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() {
|
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() {
|
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() {
|
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)
|
chatTool, _ = sjson.SetRawBytes(chatTool, "function", function)
|
||||||
chatCompletionsTools = append(chatCompletionsTools, gjson.Parse(chatTool).Value())
|
chatCompletionsTools = append(chatCompletionsTools, gjson.ParseBytes(chatTool).Value())
|
||||||
|
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if len(chatCompletionsTools) > 0 {
|
if len(chatCompletionsTools) > 0 {
|
||||||
out, _ = sjson.Set(out, "tools", chatCompletionsTools)
|
out, _ = sjson.SetBytes(out, "tools", chatCompletionsTools)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if reasoningEffort := root.Get("reasoning.effort"); reasoningEffort.Exists() {
|
if reasoningEffort := root.Get("reasoning.effort"); reasoningEffort.Exists() {
|
||||||
effort := strings.ToLower(strings.TrimSpace(reasoningEffort.String()))
|
effort := strings.ToLower(strings.TrimSpace(reasoningEffort.String()))
|
||||||
if effort != "" {
|
if effort != "" {
|
||||||
out, _ = sjson.Set(out, "reasoning_effort", effort)
|
out, _ = sjson.SetBytes(out, "reasoning_effort", effort)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert tool_choice if present
|
// Convert tool_choice if present
|
||||||
if toolChoice := root.Get("tool_choice"); toolChoice.Exists() {
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
translatorcommon "github.com/router-for-me/CLIProxyAPI/v6/internal/translator/common"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
@@ -50,13 +51,13 @@ type oaiToResponsesState struct {
|
|||||||
// responseIDCounter provides a process-wide unique counter for synthesized response identifiers.
|
// responseIDCounter provides a process-wide unique counter for synthesized response identifiers.
|
||||||
var responseIDCounter uint64
|
var responseIDCounter uint64
|
||||||
|
|
||||||
func emitRespEvent(event string, payload string) string {
|
func emitRespEvent(event string, payload []byte) []byte {
|
||||||
return fmt.Sprintf("event: %s\ndata: %s", event, payload)
|
return translatorcommon.SSEEventData(event, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertOpenAIChatCompletionsResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
// ConvertOpenAIChatCompletionsResponseToOpenAIResponses converts OpenAI Chat Completions streaming chunks
|
||||||
// to OpenAI Responses SSE events (response.*).
|
// 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 {
|
if *param == nil {
|
||||||
*param = &oaiToResponsesState{
|
*param = &oaiToResponsesState{
|
||||||
FuncArgsBuf: make(map[int]*strings.Builder),
|
FuncArgsBuf: make(map[int]*strings.Builder),
|
||||||
@@ -79,19 +80,19 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
|
|
||||||
rawJSON = bytes.TrimSpace(rawJSON)
|
rawJSON = bytes.TrimSpace(rawJSON)
|
||||||
if len(rawJSON) == 0 {
|
if len(rawJSON) == 0 {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
if bytes.Equal(rawJSON, []byte("[DONE]")) {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
obj := root.Get("object")
|
obj := root.Get("object")
|
||||||
if obj.Exists() && obj.String() != "" && obj.String() != "chat.completion.chunk" {
|
if obj.Exists() && obj.String() != "" && obj.String() != "chat.completion.chunk" {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
if !root.Get("choices").Exists() || !root.Get("choices").IsArray() {
|
if !root.Get("choices").Exists() || !root.Get("choices").IsArray() {
|
||||||
return []string{}
|
return [][]byte{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
@@ -124,7 +125,7 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
nextSeq := func() int { st.Seq++; return st.Seq }
|
nextSeq := func() int { st.Seq++; return st.Seq }
|
||||||
var out []string
|
var out [][]byte
|
||||||
|
|
||||||
if !st.Started {
|
if !st.Started {
|
||||||
st.ResponseID = root.Get("id").String()
|
st.ResponseID = root.Get("id").String()
|
||||||
@@ -149,39 +150,39 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
st.ReasoningTokens = 0
|
st.ReasoningTokens = 0
|
||||||
st.UsageSeen = false
|
st.UsageSeen = false
|
||||||
// response.created
|
// 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 := []byte(`{"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.SetBytes(created, "sequence_number", nextSeq())
|
||||||
created, _ = sjson.Set(created, "response.id", st.ResponseID)
|
created, _ = sjson.SetBytes(created, "response.id", st.ResponseID)
|
||||||
created, _ = sjson.Set(created, "response.created_at", st.Created)
|
created, _ = sjson.SetBytes(created, "response.created_at", st.Created)
|
||||||
out = append(out, emitRespEvent("response.created", 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 := []byte(`{"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.SetBytes(inprog, "sequence_number", nextSeq())
|
||||||
inprog, _ = sjson.Set(inprog, "response.id", st.ResponseID)
|
inprog, _ = sjson.SetBytes(inprog, "response.id", st.ResponseID)
|
||||||
inprog, _ = sjson.Set(inprog, "response.created_at", st.Created)
|
inprog, _ = sjson.SetBytes(inprog, "response.created_at", st.Created)
|
||||||
out = append(out, emitRespEvent("response.in_progress", inprog))
|
out = append(out, emitRespEvent("response.in_progress", inprog))
|
||||||
st.Started = true
|
st.Started = true
|
||||||
}
|
}
|
||||||
|
|
||||||
stopReasoning := func(text string) {
|
stopReasoning := func(text string) {
|
||||||
// Emit reasoning done events
|
// Emit reasoning done events
|
||||||
textDone := `{"type":"response.reasoning_summary_text.done","sequence_number":0,"item_id":"","output_index":0,"summary_index":0,"text":""}`
|
textDone := []byte(`{"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.SetBytes(textDone, "sequence_number", nextSeq())
|
||||||
textDone, _ = sjson.Set(textDone, "item_id", st.ReasoningID)
|
textDone, _ = sjson.SetBytes(textDone, "item_id", st.ReasoningID)
|
||||||
textDone, _ = sjson.Set(textDone, "output_index", st.ReasoningIndex)
|
textDone, _ = sjson.SetBytes(textDone, "output_index", st.ReasoningIndex)
|
||||||
textDone, _ = sjson.Set(textDone, "text", text)
|
textDone, _ = sjson.SetBytes(textDone, "text", text)
|
||||||
out = append(out, emitRespEvent("response.reasoning_summary_text.done", textDone))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", st.ReasoningID)
|
partDone, _ = sjson.SetBytes(partDone, "item_id", st.ReasoningID)
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", st.ReasoningIndex)
|
partDone, _ = sjson.SetBytes(partDone, "output_index", st.ReasoningIndex)
|
||||||
partDone, _ = sjson.Set(partDone, "part.text", text)
|
partDone, _ = sjson.SetBytes(partDone, "part.text", text)
|
||||||
out = append(out, emitRespEvent("response.reasoning_summary_part.done", partDone))
|
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 := []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.Set(outputItemDone, "sequence_number", nextSeq())
|
outputItemDone, _ = sjson.SetBytes(outputItemDone, "sequence_number", nextSeq())
|
||||||
outputItemDone, _ = sjson.Set(outputItemDone, "item.id", st.ReasoningID)
|
outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.id", st.ReasoningID)
|
||||||
outputItemDone, _ = sjson.Set(outputItemDone, "output_index", st.ReasoningIndex)
|
outputItemDone, _ = sjson.SetBytes(outputItemDone, "output_index", st.ReasoningIndex)
|
||||||
outputItemDone, _ = sjson.Set(outputItemDone, "item.summary.text", text)
|
outputItemDone, _ = sjson.SetBytes(outputItemDone, "item.summary.text", text)
|
||||||
out = append(out, emitRespEvent("response.output_item.done", outputItemDone))
|
out = append(out, emitRespEvent("response.output_item.done", outputItemDone))
|
||||||
|
|
||||||
st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text})
|
st.Reasonings = append(st.Reasonings, oaiToResponsesStateReasoning{ReasoningID: st.ReasoningID, ReasoningData: text})
|
||||||
@@ -201,29 +202,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
st.ReasoningBuf.Reset()
|
st.ReasoningBuf.Reset()
|
||||||
}
|
}
|
||||||
if !st.MsgItemAdded[idx] {
|
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 := []byte(`{"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.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", idx)
|
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||||
item, _ = sjson.Set(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
item, _ = sjson.SetBytes(item, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||||
out = append(out, emitRespEvent("response.output_item.added", item))
|
out = append(out, emitRespEvent("response.output_item.added", item))
|
||||||
st.MsgItemAdded[idx] = true
|
st.MsgItemAdded[idx] = true
|
||||||
}
|
}
|
||||||
if !st.MsgContentAdded[idx] {
|
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 := []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.Set(part, "sequence_number", nextSeq())
|
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
|
||||||
part, _ = sjson.Set(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
part, _ = sjson.SetBytes(part, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||||
part, _ = sjson.Set(part, "output_index", idx)
|
part, _ = sjson.SetBytes(part, "output_index", idx)
|
||||||
part, _ = sjson.Set(part, "content_index", 0)
|
part, _ = sjson.SetBytes(part, "content_index", 0)
|
||||||
out = append(out, emitRespEvent("response.content_part.added", part))
|
out = append(out, emitRespEvent("response.content_part.added", part))
|
||||||
st.MsgContentAdded[idx] = true
|
st.MsgContentAdded[idx] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := `{"type":"response.output_text.delta","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"delta":"","logprobs":[]}`
|
msg := []byte(`{"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.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
msg, _ = sjson.SetBytes(msg, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||||
msg, _ = sjson.Set(msg, "output_index", idx)
|
msg, _ = sjson.SetBytes(msg, "output_index", idx)
|
||||||
msg, _ = sjson.Set(msg, "content_index", 0)
|
msg, _ = sjson.SetBytes(msg, "content_index", 0)
|
||||||
msg, _ = sjson.Set(msg, "delta", c.String())
|
msg, _ = sjson.SetBytes(msg, "delta", c.String())
|
||||||
out = append(out, emitRespEvent("response.output_text.delta", msg))
|
out = append(out, emitRespEvent("response.output_text.delta", msg))
|
||||||
// aggregate for response.output
|
// aggregate for response.output
|
||||||
if st.MsgTextBuf[idx] == nil {
|
if st.MsgTextBuf[idx] == nil {
|
||||||
@@ -238,24 +239,24 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
if st.ReasoningID == "" {
|
if st.ReasoningID == "" {
|
||||||
st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx)
|
st.ReasoningID = fmt.Sprintf("rs_%s_%d", st.ResponseID, idx)
|
||||||
st.ReasoningIndex = 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 := []byte(`{"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.SetBytes(item, "sequence_number", nextSeq())
|
||||||
item, _ = sjson.Set(item, "output_index", idx)
|
item, _ = sjson.SetBytes(item, "output_index", idx)
|
||||||
item, _ = sjson.Set(item, "item.id", st.ReasoningID)
|
item, _ = sjson.SetBytes(item, "item.id", st.ReasoningID)
|
||||||
out = append(out, emitRespEvent("response.output_item.added", item))
|
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 := []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.Set(part, "sequence_number", nextSeq())
|
part, _ = sjson.SetBytes(part, "sequence_number", nextSeq())
|
||||||
part, _ = sjson.Set(part, "item_id", st.ReasoningID)
|
part, _ = sjson.SetBytes(part, "item_id", st.ReasoningID)
|
||||||
part, _ = sjson.Set(part, "output_index", st.ReasoningIndex)
|
part, _ = sjson.SetBytes(part, "output_index", st.ReasoningIndex)
|
||||||
out = append(out, emitRespEvent("response.reasoning_summary_part.added", part))
|
out = append(out, emitRespEvent("response.reasoning_summary_part.added", part))
|
||||||
}
|
}
|
||||||
// Append incremental text to reasoning buffer
|
// Append incremental text to reasoning buffer
|
||||||
st.ReasoningBuf.WriteString(rc.String())
|
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 := []byte(`{"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.SetBytes(msg, "sequence_number", nextSeq())
|
||||||
msg, _ = sjson.Set(msg, "item_id", st.ReasoningID)
|
msg, _ = sjson.SetBytes(msg, "item_id", st.ReasoningID)
|
||||||
msg, _ = sjson.Set(msg, "output_index", st.ReasoningIndex)
|
msg, _ = sjson.SetBytes(msg, "output_index", st.ReasoningIndex)
|
||||||
msg, _ = sjson.Set(msg, "delta", rc.String())
|
msg, _ = sjson.SetBytes(msg, "delta", rc.String())
|
||||||
out = append(out, emitRespEvent("response.reasoning_summary_text.delta", msg))
|
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 {
|
if b := st.MsgTextBuf[idx]; b != nil {
|
||||||
fullText = b.String()
|
fullText = b.String()
|
||||||
}
|
}
|
||||||
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
done := []byte(`{"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.SetBytes(done, "sequence_number", nextSeq())
|
||||||
done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||||
done, _ = sjson.Set(done, "output_index", idx)
|
done, _ = sjson.SetBytes(done, "output_index", idx)
|
||||||
done, _ = sjson.Set(done, "content_index", 0)
|
done, _ = sjson.SetBytes(done, "content_index", 0)
|
||||||
done, _ = sjson.Set(done, "text", fullText)
|
done, _ = sjson.SetBytes(done, "text", fullText)
|
||||||
out = append(out, emitRespEvent("response.output_text.done", done))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", idx)
|
partDone, _ = sjson.SetBytes(partDone, "output_index", idx)
|
||||||
partDone, _ = sjson.Set(partDone, "content_index", 0)
|
partDone, _ = sjson.SetBytes(partDone, "content_index", 0)
|
||||||
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
|
||||||
out = append(out, emitRespEvent("response.content_part.done", partDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", idx)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", idx)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, idx))
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText)
|
||||||
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
||||||
st.MsgItemDone[idx] = true
|
st.MsgItemDone[idx] = true
|
||||||
}
|
}
|
||||||
@@ -314,13 +315,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if shouldEmitItem && effectiveCallID != "" {
|
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 := []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.Set(o, "sequence_number", nextSeq())
|
o, _ = sjson.SetBytes(o, "sequence_number", nextSeq())
|
||||||
o, _ = sjson.Set(o, "output_index", idx)
|
o, _ = sjson.SetBytes(o, "output_index", idx)
|
||||||
o, _ = sjson.Set(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID))
|
o, _ = sjson.SetBytes(o, "item.id", fmt.Sprintf("fc_%s", effectiveCallID))
|
||||||
o, _ = sjson.Set(o, "item.call_id", effectiveCallID)
|
o, _ = sjson.SetBytes(o, "item.call_id", effectiveCallID)
|
||||||
name := st.FuncNames[idx]
|
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))
|
out = append(out, emitRespEvent("response.output_item.added", o))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,11 +338,11 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
refCallID = newCallID
|
refCallID = newCallID
|
||||||
}
|
}
|
||||||
if refCallID != "" {
|
if refCallID != "" {
|
||||||
ad := `{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`
|
ad := []byte(`{"type":"response.function_call_arguments.delta","sequence_number":0,"item_id":"","output_index":0,"delta":""}`)
|
||||||
ad, _ = sjson.Set(ad, "sequence_number", nextSeq())
|
ad, _ = sjson.SetBytes(ad, "sequence_number", nextSeq())
|
||||||
ad, _ = sjson.Set(ad, "item_id", fmt.Sprintf("fc_%s", refCallID))
|
ad, _ = sjson.SetBytes(ad, "item_id", fmt.Sprintf("fc_%s", refCallID))
|
||||||
ad, _ = sjson.Set(ad, "output_index", idx)
|
ad, _ = sjson.SetBytes(ad, "output_index", idx)
|
||||||
ad, _ = sjson.Set(ad, "delta", args.String())
|
ad, _ = sjson.SetBytes(ad, "delta", args.String())
|
||||||
out = append(out, emitRespEvent("response.function_call_arguments.delta", ad))
|
out = append(out, emitRespEvent("response.function_call_arguments.delta", ad))
|
||||||
}
|
}
|
||||||
st.FuncArgsBuf[idx].WriteString(args.String())
|
st.FuncArgsBuf[idx].WriteString(args.String())
|
||||||
@@ -372,27 +373,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
if b := st.MsgTextBuf[i]; b != nil {
|
if b := st.MsgTextBuf[i]; b != nil {
|
||||||
fullText = b.String()
|
fullText = b.String()
|
||||||
}
|
}
|
||||||
done := `{"type":"response.output_text.done","sequence_number":0,"item_id":"","output_index":0,"content_index":0,"text":"","logprobs":[]}`
|
done := []byte(`{"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.SetBytes(done, "sequence_number", nextSeq())
|
||||||
done, _ = sjson.Set(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
done, _ = sjson.SetBytes(done, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||||
done, _ = sjson.Set(done, "output_index", i)
|
done, _ = sjson.SetBytes(done, "output_index", i)
|
||||||
done, _ = sjson.Set(done, "content_index", 0)
|
done, _ = sjson.SetBytes(done, "content_index", 0)
|
||||||
done, _ = sjson.Set(done, "text", fullText)
|
done, _ = sjson.SetBytes(done, "text", fullText)
|
||||||
out = append(out, emitRespEvent("response.output_text.done", done))
|
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 := []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.Set(partDone, "sequence_number", nextSeq())
|
partDone, _ = sjson.SetBytes(partDone, "sequence_number", nextSeq())
|
||||||
partDone, _ = sjson.Set(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
partDone, _ = sjson.SetBytes(partDone, "item_id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||||
partDone, _ = sjson.Set(partDone, "output_index", i)
|
partDone, _ = sjson.SetBytes(partDone, "output_index", i)
|
||||||
partDone, _ = sjson.Set(partDone, "content_index", 0)
|
partDone, _ = sjson.SetBytes(partDone, "content_index", 0)
|
||||||
partDone, _ = sjson.Set(partDone, "part.text", fullText)
|
partDone, _ = sjson.SetBytes(partDone, "part.text", fullText)
|
||||||
out = append(out, emitRespEvent("response.content_part.done", partDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", i)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", i)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.content.0.text", fullText)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.content.0.text", fullText)
|
||||||
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
||||||
st.MsgItemDone[i] = true
|
st.MsgItemDone[i] = true
|
||||||
}
|
}
|
||||||
@@ -426,101 +427,101 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 {
|
if b := st.FuncArgsBuf[i]; b != nil && b.Len() > 0 {
|
||||||
args = b.String()
|
args = b.String()
|
||||||
}
|
}
|
||||||
fcDone := `{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`
|
fcDone := []byte(`{"type":"response.function_call_arguments.done","sequence_number":0,"item_id":"","output_index":0,"arguments":""}`)
|
||||||
fcDone, _ = sjson.Set(fcDone, "sequence_number", nextSeq())
|
fcDone, _ = sjson.SetBytes(fcDone, "sequence_number", nextSeq())
|
||||||
fcDone, _ = sjson.Set(fcDone, "item_id", fmt.Sprintf("fc_%s", callID))
|
fcDone, _ = sjson.SetBytes(fcDone, "item_id", fmt.Sprintf("fc_%s", callID))
|
||||||
fcDone, _ = sjson.Set(fcDone, "output_index", i)
|
fcDone, _ = sjson.SetBytes(fcDone, "output_index", i)
|
||||||
fcDone, _ = sjson.Set(fcDone, "arguments", args)
|
fcDone, _ = sjson.SetBytes(fcDone, "arguments", args)
|
||||||
out = append(out, emitRespEvent("response.function_call_arguments.done", fcDone))
|
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 := []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.Set(itemDone, "sequence_number", nextSeq())
|
itemDone, _ = sjson.SetBytes(itemDone, "sequence_number", nextSeq())
|
||||||
itemDone, _ = sjson.Set(itemDone, "output_index", i)
|
itemDone, _ = sjson.SetBytes(itemDone, "output_index", i)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.id", fmt.Sprintf("fc_%s", callID))
|
itemDone, _ = sjson.SetBytes(itemDone, "item.id", fmt.Sprintf("fc_%s", callID))
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.arguments", args)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.arguments", args)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.call_id", callID)
|
itemDone, _ = sjson.SetBytes(itemDone, "item.call_id", callID)
|
||||||
itemDone, _ = sjson.Set(itemDone, "item.name", st.FuncNames[i])
|
itemDone, _ = sjson.SetBytes(itemDone, "item.name", st.FuncNames[i])
|
||||||
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
out = append(out, emitRespEvent("response.output_item.done", itemDone))
|
||||||
st.FuncItemDone[i] = true
|
st.FuncItemDone[i] = true
|
||||||
st.FuncArgsDone[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 := []byte(`{"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.SetBytes(completed, "sequence_number", nextSeq())
|
||||||
completed, _ = sjson.Set(completed, "response.id", st.ResponseID)
|
completed, _ = sjson.SetBytes(completed, "response.id", st.ResponseID)
|
||||||
completed, _ = sjson.Set(completed, "response.created_at", st.Created)
|
completed, _ = sjson.SetBytes(completed, "response.created_at", st.Created)
|
||||||
// Inject original request fields into response as per docs/response.completed.json
|
// Inject original request fields into response as per docs/response.completed.json
|
||||||
if requestRawJSON != nil {
|
if requestRawJSON != nil {
|
||||||
req := gjson.ParseBytes(requestRawJSON)
|
req := gjson.ParseBytes(requestRawJSON)
|
||||||
if v := req.Get("instructions"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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
|
// Build response.output using aggregated buffers
|
||||||
outputsWrapper := `{"arr":[]}`
|
outputsWrapper := []byte(`{"arr":[]}`)
|
||||||
if len(st.Reasonings) > 0 {
|
if len(st.Reasonings) > 0 {
|
||||||
for _, r := range st.Reasonings {
|
for _, r := range st.Reasonings {
|
||||||
item := `{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`
|
item := []byte(`{"id":"","type":"reasoning","summary":[{"type":"summary_text","text":""}]}`)
|
||||||
item, _ = sjson.Set(item, "id", r.ReasoningID)
|
item, _ = sjson.SetBytes(item, "id", r.ReasoningID)
|
||||||
item, _ = sjson.Set(item, "summary.0.text", r.ReasoningData)
|
item, _ = sjson.SetBytes(item, "summary.0.text", r.ReasoningData)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Append message items in ascending index order
|
// Append message items in ascending index order
|
||||||
@@ -541,10 +542,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
if b := st.MsgTextBuf[i]; b != nil {
|
if b := st.MsgTextBuf[i]; b != nil {
|
||||||
txt = b.String()
|
txt = b.String()
|
||||||
}
|
}
|
||||||
item := `{"id":"","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":""}],"role":"assistant"}`
|
item := []byte(`{"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.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", st.ResponseID, i))
|
||||||
item, _ = sjson.Set(item, "content.0.text", txt)
|
item, _ = sjson.SetBytes(item, "content.0.text", txt)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(st.FuncArgsBuf) > 0 {
|
if len(st.FuncArgsBuf) > 0 {
|
||||||
@@ -567,29 +568,29 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
}
|
}
|
||||||
callID := st.FuncCallIDs[i]
|
callID := st.FuncCallIDs[i]
|
||||||
name := st.FuncNames[i]
|
name := st.FuncNames[i]
|
||||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
item, _ = sjson.Set(item, "arguments", args)
|
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||||
item, _ = sjson.Set(item, "call_id", callID)
|
item, _ = sjson.SetBytes(item, "call_id", callID)
|
||||||
item, _ = sjson.Set(item, "name", name)
|
item, _ = sjson.SetBytes(item, "name", name)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
completed, _ = sjson.SetRaw(completed, "response.output", gjson.Get(outputsWrapper, "arr").Raw)
|
completed, _ = sjson.SetRawBytes(completed, "response.output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||||
}
|
}
|
||||||
if st.UsageSeen {
|
if st.UsageSeen {
|
||||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens", st.PromptTokens)
|
completed, _ = sjson.SetBytes(completed, "response.usage.input_tokens", st.PromptTokens)
|
||||||
completed, _ = sjson.Set(completed, "response.usage.input_tokens_details.cached_tokens", st.CachedTokens)
|
completed, _ = sjson.SetBytes(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.output_tokens", st.CompletionTokens)
|
||||||
if st.ReasoningTokens > 0 {
|
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
|
total := st.TotalTokens
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
total = st.PromptTokens + st.CompletionTokens
|
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))
|
out = append(out, emitRespEvent("response.completed", completed))
|
||||||
}
|
}
|
||||||
@@ -603,103 +604,103 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx context.Context,
|
|||||||
|
|
||||||
// ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream builds a single Responses JSON
|
// ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream builds a single Responses JSON
|
||||||
// from a non-streaming OpenAI Chat Completions response.
|
// 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)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Basic response scaffold
|
// 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: use provider id if present, otherwise synthesize
|
||||||
id := root.Get("id").String()
|
id := root.Get("id").String()
|
||||||
if id == "" {
|
if id == "" {
|
||||||
id = fmt.Sprintf("resp_%x_%d", time.Now().UnixNano(), atomic.AddUint64(&responseIDCounter, 1))
|
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_at: map from chat.completion created
|
||||||
created := root.Get("created").Int()
|
created := root.Get("created").Int()
|
||||||
if created == 0 {
|
if created == 0 {
|
||||||
created = time.Now().Unix()
|
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)
|
// Echo request fields when available (aligns with streaming path behavior)
|
||||||
if len(requestRawJSON) > 0 {
|
if len(requestRawJSON) > 0 {
|
||||||
req := gjson.ParseBytes(requestRawJSON)
|
req := gjson.ParseBytes(requestRawJSON)
|
||||||
if v := req.Get("instructions"); v.Exists() {
|
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() {
|
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 {
|
} else {
|
||||||
// Also support max_tokens from chat completion style
|
// Also support max_tokens from chat completion style
|
||||||
if v = req.Get("max_tokens"); v.Exists() {
|
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() {
|
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() {
|
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() {
|
} 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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
} else if v := root.Get("model"); v.Exists() {
|
||||||
// Fallback model from response
|
// Fallback model from response
|
||||||
resp, _ = sjson.Set(resp, "model", v.String())
|
resp, _ = sjson.SetBytes(resp, "model", v.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build output list from choices[...]
|
// Build output list from choices[...]
|
||||||
outputsWrapper := `{"arr":[]}`
|
outputsWrapper := []byte(`{"arr":[]}`)
|
||||||
// Detect and capture reasoning content if present
|
// Detect and capture reasoning content if present
|
||||||
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
|
rcText := gjson.GetBytes(rawJSON, "choices.0.message.reasoning_content").String()
|
||||||
includeReasoning := rcText != ""
|
includeReasoning := rcText != ""
|
||||||
@@ -712,13 +713,13 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
rid = strings.TrimPrefix(rid, "resp_")
|
rid = strings.TrimPrefix(rid, "resp_")
|
||||||
}
|
}
|
||||||
// Prefer summary_text from reasoning_content; encrypted_content is optional
|
// Prefer summary_text from reasoning_content; encrypted_content is optional
|
||||||
reasoningItem := `{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`
|
reasoningItem := []byte(`{"id":"","type":"reasoning","encrypted_content":"","summary":[]}`)
|
||||||
reasoningItem, _ = sjson.Set(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
|
reasoningItem, _ = sjson.SetBytes(reasoningItem, "id", fmt.Sprintf("rs_%s", rid))
|
||||||
if rcText != "" {
|
if rcText != "" {
|
||||||
reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.type", "summary_text")
|
reasoningItem, _ = sjson.SetBytes(reasoningItem, "summary.0.type", "summary_text")
|
||||||
reasoningItem, _ = sjson.Set(reasoningItem, "summary.0.text", rcText)
|
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() {
|
if choices := root.Get("choices"); choices.Exists() && choices.IsArray() {
|
||||||
@@ -727,10 +728,10 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
if msg.Exists() {
|
if msg.Exists() {
|
||||||
// Text message part
|
// Text message part
|
||||||
if c := msg.Get("content"); c.Exists() && c.String() != "" {
|
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 := []byte(`{"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.SetBytes(item, "id", fmt.Sprintf("msg_%s_%d", id, int(choice.Get("index").Int())))
|
||||||
item, _ = sjson.Set(item, "content.0.text", c.String())
|
item, _ = sjson.SetBytes(item, "content.0.text", c.String())
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function/tool calls
|
// Function/tool calls
|
||||||
@@ -739,12 +740,12 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
callID := tc.Get("id").String()
|
callID := tc.Get("id").String()
|
||||||
name := tc.Get("function.name").String()
|
name := tc.Get("function.name").String()
|
||||||
args := tc.Get("function.arguments").String()
|
args := tc.Get("function.arguments").String()
|
||||||
item := `{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`
|
item := []byte(`{"id":"","type":"function_call","status":"completed","arguments":"","call_id":"","name":""}`)
|
||||||
item, _ = sjson.Set(item, "id", fmt.Sprintf("fc_%s", callID))
|
item, _ = sjson.SetBytes(item, "id", fmt.Sprintf("fc_%s", callID))
|
||||||
item, _ = sjson.Set(item, "arguments", args)
|
item, _ = sjson.SetBytes(item, "arguments", args)
|
||||||
item, _ = sjson.Set(item, "call_id", callID)
|
item, _ = sjson.SetBytes(item, "call_id", callID)
|
||||||
item, _ = sjson.Set(item, "name", name)
|
item, _ = sjson.SetBytes(item, "name", name)
|
||||||
outputsWrapper, _ = sjson.SetRaw(outputsWrapper, "arr.-1", item)
|
outputsWrapper, _ = sjson.SetRawBytes(outputsWrapper, "arr.-1", item)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -752,27 +753,27 @@ func ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(_ context.Co
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if gjson.Get(outputsWrapper, "arr.#").Int() > 0 {
|
if gjson.GetBytes(outputsWrapper, "arr.#").Int() > 0 {
|
||||||
resp, _ = sjson.SetRaw(resp, "output", gjson.Get(outputsWrapper, "arr").Raw)
|
resp, _ = sjson.SetRawBytes(resp, "output", []byte(gjson.GetBytes(outputsWrapper, "arr").Raw))
|
||||||
}
|
}
|
||||||
|
|
||||||
// usage mapping
|
// usage mapping
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
if usage := root.Get("usage"); usage.Exists() {
|
||||||
// Map common tokens
|
// Map common tokens
|
||||||
if usage.Get("prompt_tokens").Exists() || usage.Get("completion_tokens").Exists() || usage.Get("total_tokens").Exists() {
|
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() {
|
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
|
// 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() {
|
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 {
|
} else {
|
||||||
// Fallback to raw usage object if structure differs
|
// Fallback to raw usage object if structure differs
|
||||||
resp, _ = sjson.Set(resp, "usage", usage.Value())
|
resp, _ = sjson.SetBytes(resp, "usage", usage.Value())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ func NeedConvert(from, to string) bool {
|
|||||||
// - param: Additional parameters for translation
|
// - param: Additional parameters for translation
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - []string: The translated response lines
|
// - [][]byte: The translated response lines
|
||||||
func Response(from, to string, ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) []string {
|
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)
|
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
|
// - param: Additional parameters for translation
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - string: The translated response JSON
|
// - []byte: The translated response JSON
|
||||||
func ResponseNonStream(from, to string, ctx context.Context, modelName string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string {
|
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)
|
return registry.TranslateNonStream(ctx, sdktranslator.FromString(from), sdktranslator.FromString(to), modelName, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ func removePlaceholderFields(jsonStr string) string {
|
|||||||
if len(filtered) == 0 {
|
if len(filtered) == 0 {
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
|
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
|
||||||
} else {
|
} 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 {
|
if len(filtered) == 0 {
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
|
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
|
||||||
} else {
|
} 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 := `{"type":"object","description":""}`
|
||||||
replacement, _ = sjson.Set(replacement, "description", hint)
|
replacementBytes, _ := sjson.SetBytes([]byte(replacement), "description", hint)
|
||||||
|
replacement = string(replacementBytes)
|
||||||
jsonStr = setRawAt(jsonStr, parentPath, replacement)
|
jsonStr = setRawAt(jsonStr, parentPath, replacement)
|
||||||
}
|
}
|
||||||
return jsonStr
|
return jsonStr
|
||||||
@@ -176,7 +179,8 @@ func convertConstToEnum(jsonStr string) string {
|
|||||||
}
|
}
|
||||||
enumPath := trimSuffix(p, ".const") + ".enum"
|
enumPath := trimSuffix(p, ".const") + ".enum"
|
||||||
if !gjson.Get(jsonStr, enumPath).Exists() {
|
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
|
return jsonStr
|
||||||
@@ -198,9 +202,11 @@ func convertEnumValuesToStrings(jsonStr string) string {
|
|||||||
|
|
||||||
// Always update enum values to strings and set type to "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
|
// 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")
|
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
|
return jsonStr
|
||||||
}
|
}
|
||||||
@@ -236,7 +242,7 @@ func addAdditionalPropertiesHints(jsonStr string) string {
|
|||||||
|
|
||||||
var unsupportedConstraints = []string{
|
var unsupportedConstraints = []string{
|
||||||
"minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
|
"minLength", "maxLength", "exclusiveMinimum", "exclusiveMaximum",
|
||||||
"pattern", "minItems", "maxItems", "format",
|
"pattern", "minItems", "maxItems", "uniqueItems", "format",
|
||||||
"default", "examples", // Claude rejects these in VALIDATED mode
|
"default", "examples", // Claude rejects these in VALIDATED mode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +279,8 @@ func mergeAllOf(jsonStr string) string {
|
|||||||
if props := item.Get("properties"); props.IsObject() {
|
if props := item.Get("properties"); props.IsObject() {
|
||||||
props.ForEach(func(key, value gjson.Result) bool {
|
props.ForEach(func(key, value gjson.Result) bool {
|
||||||
destPath := joinPath(parentPath, "properties."+escapeGJSONPathKey(key.String()))
|
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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -285,7 +292,8 @@ func mergeAllOf(jsonStr string) string {
|
|||||||
current = append(current, s)
|
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)
|
jsonStr, _ = sjson.Delete(jsonStr, p)
|
||||||
@@ -381,7 +389,8 @@ func flattenTypeArrays(jsonStr string) string {
|
|||||||
firstType = nonNullTypes[0]
|
firstType = nonNullTypes[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonStr, _ = sjson.Set(jsonStr, p, firstType)
|
updated, _ := sjson.SetBytes([]byte(jsonStr), p, firstType)
|
||||||
|
jsonStr = string(updated)
|
||||||
|
|
||||||
parentPath := trimSuffix(p, ".type")
|
parentPath := trimSuffix(p, ".type")
|
||||||
if len(nonNullTypes) > 1 {
|
if len(nonNullTypes) > 1 {
|
||||||
@@ -420,7 +429,8 @@ func flattenTypeArrays(jsonStr string) string {
|
|||||||
if len(filtered) == 0 {
|
if len(filtered) == 0 {
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
|
jsonStr, _ = sjson.Delete(jsonStr, reqPath)
|
||||||
} else {
|
} else {
|
||||||
jsonStr, _ = sjson.Set(jsonStr, reqPath, filtered)
|
updated, _ := sjson.SetBytes([]byte(jsonStr), reqPath, filtered)
|
||||||
|
jsonStr = string(updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return jsonStr
|
return jsonStr
|
||||||
@@ -518,7 +528,8 @@ func cleanupRequiredFields(jsonStr string) string {
|
|||||||
if len(valid) == 0 {
|
if len(valid) == 0 {
|
||||||
jsonStr, _ = sjson.Delete(jsonStr, p)
|
jsonStr, _ = sjson.Delete(jsonStr, p)
|
||||||
} else {
|
} 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 {
|
if needsPlaceholder {
|
||||||
// Add placeholder "reason" property
|
// Add placeholder "reason" property
|
||||||
reasonPath := joinPath(propsPath, "reason")
|
reasonPath := joinPath(propsPath, "reason")
|
||||||
jsonStr, _ = sjson.Set(jsonStr, reasonPath+".type", "string")
|
updated, _ := sjson.SetBytes([]byte(jsonStr), reasonPath+".type", "string")
|
||||||
jsonStr, _ = sjson.Set(jsonStr, reasonPath+".description", placeholderReasonDescription)
|
jsonStr = string(updated)
|
||||||
|
updated, _ = sjson.SetBytes([]byte(jsonStr), reasonPath+".description", placeholderReasonDescription)
|
||||||
|
jsonStr = string(updated)
|
||||||
|
|
||||||
// Add to required array
|
// Add to required array
|
||||||
jsonStr, _ = sjson.Set(jsonStr, reqPath, []string{"reason"})
|
updated, _ = sjson.SetBytes([]byte(jsonStr), reqPath, []string{"reason"})
|
||||||
|
jsonStr = string(updated)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,9 +593,11 @@ func addEmptySchemaPlaceholder(jsonStr string) string {
|
|||||||
}
|
}
|
||||||
placeholderPath := joinPath(propsPath, "_")
|
placeholderPath := joinPath(propsPath, "_")
|
||||||
if !gjson.Get(jsonStr, placeholderPath).Exists() {
|
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 == "" {
|
if path == "" {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
result, _ := sjson.SetRaw(jsonStr, path, value)
|
result, _ := sjson.SetRawBytes([]byte(jsonStr), path, []byte(value))
|
||||||
return result
|
return string(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isPropertyDefinition(path string) bool {
|
func isPropertyDefinition(path string) bool {
|
||||||
@@ -678,7 +694,8 @@ func appendHint(jsonStr, parentPath, hint string) string {
|
|||||||
if existing != "" {
|
if existing != "" {
|
||||||
hint = fmt.Sprintf("%s (%s)", existing, hint)
|
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
|
return jsonStr
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +704,8 @@ func appendHintRaw(jsonRaw, hint string) string {
|
|||||||
if existing != "" {
|
if existing != "" {
|
||||||
hint = fmt.Sprintf("%s (%s)", existing, hint)
|
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
|
return jsonRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,13 +791,13 @@ func mergeDescriptionRaw(schemaRaw, parentDesc string) string {
|
|||||||
childDesc := gjson.Get(schemaRaw, "description").String()
|
childDesc := gjson.Get(schemaRaw, "description").String()
|
||||||
switch {
|
switch {
|
||||||
case childDesc == "":
|
case childDesc == "":
|
||||||
schemaRaw, _ = sjson.Set(schemaRaw, "description", parentDesc)
|
updated, _ := sjson.SetBytes([]byte(schemaRaw), "description", parentDesc)
|
||||||
return schemaRaw
|
return string(updated)
|
||||||
case childDesc == parentDesc:
|
case childDesc == parentDesc:
|
||||||
return schemaRaw
|
return schemaRaw
|
||||||
default:
|
default:
|
||||||
combined := fmt.Sprintf("%s (%s)", parentDesc, childDesc)
|
combined := fmt.Sprintf("%s (%s)", parentDesc, childDesc)
|
||||||
schemaRaw, _ = sjson.Set(schemaRaw, "description", combined)
|
updated, _ := sjson.SetBytes([]byte(schemaRaw), "description", combined)
|
||||||
return schemaRaw
|
return string(updated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -74,17 +74,17 @@ func RenameKey(jsonStr, oldKeyPath, newKeyPath string) (string, error) {
|
|||||||
return "", fmt.Errorf("old key '%s' does not exist", oldKeyPath)
|
return "", fmt.Errorf("old key '%s' does not exist", oldKeyPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
interimJson, err := sjson.SetRaw(jsonStr, newKeyPath, value.Raw)
|
interimJSON, errSet := sjson.SetRawBytes([]byte(jsonStr), newKeyPath, []byte(value.Raw))
|
||||||
if err != nil {
|
if errSet != nil {
|
||||||
return "", fmt.Errorf("failed to set new key '%s': %w", newKeyPath, err)
|
return "", fmt.Errorf("failed to set new key '%s': %w", newKeyPath, errSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalJson, err := sjson.Delete(interimJson, oldKeyPath)
|
finalJSON, errDelete := sjson.DeleteBytes(interimJSON, oldKeyPath)
|
||||||
if err != nil {
|
if errDelete != nil {
|
||||||
return "", fmt.Errorf("failed to delete old key '%s': %w", oldKeyPath, err)
|
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
|
// FixJSON converts non-standard JSON that uses single quotes for strings into
|
||||||
|
|||||||
@@ -209,58 +209,58 @@ func convertCompletionsRequestToChatCompletions(rawJSON []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create chat completions structure
|
// Create chat completions structure
|
||||||
out := `{"model":"","messages":[{"role":"user","content":""}]}`
|
out := []byte(`{"model":"","messages":[{"role":"user","content":""}]}`)
|
||||||
|
|
||||||
// Set model
|
// Set model
|
||||||
if model := root.Get("model"); model.Exists() {
|
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
|
// 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
|
// Copy other parameters from completions to chat completions
|
||||||
if maxTokens := root.Get("max_tokens"); maxTokens.Exists() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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() {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func convertResponsesObjectToChatCompletion(ctx context.Context, modelName string, originalChatJSON, responsesRequestJSON, responsesPayload []byte) []byte {
|
func convertResponsesObjectToChatCompletion(ctx context.Context, modelName string, originalChatJSON, responsesRequestJSON, responsesPayload []byte) []byte {
|
||||||
@@ -273,10 +273,10 @@ func convertResponsesObjectToChatCompletion(ctx context.Context, modelName strin
|
|||||||
}
|
}
|
||||||
var param any
|
var param any
|
||||||
converted := codexconverter.ConvertCodexResponseToOpenAINonStream(ctx, modelName, originalChatJSON, responsesRequestJSON, wrapped, ¶m)
|
converted := codexconverter.ConvertCodexResponseToOpenAINonStream(ctx, modelName, originalChatJSON, responsesRequestJSON, wrapped, ¶m)
|
||||||
if converted == "" {
|
if len(converted) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return []byte(converted)
|
return converted
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapResponsesPayloadAsCompleted(payload []byte) []byte {
|
func wrapResponsesPayloadAsCompleted(payload []byte) []byte {
|
||||||
@@ -294,7 +294,7 @@ func wrapResponsesPayloadAsCompleted(payload []byte) []byte {
|
|||||||
func writeConvertedResponsesChunk(c *gin.Context, ctx context.Context, modelName string, originalChatJSON, responsesRequestJSON, chunk []byte, param *any) {
|
func writeConvertedResponsesChunk(c *gin.Context, ctx context.Context, modelName string, originalChatJSON, responsesRequestJSON, chunk []byte, param *any) {
|
||||||
outputs := codexconverter.ConvertCodexResponseToOpenAI(ctx, modelName, originalChatJSON, responsesRequestJSON, chunk, param)
|
outputs := codexconverter.ConvertCodexResponseToOpenAI(ctx, modelName, originalChatJSON, responsesRequestJSON, chunk, param)
|
||||||
for _, out := range outputs {
|
for _, out := range outputs {
|
||||||
if out == "" {
|
if len(out) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", out)
|
_, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", out)
|
||||||
@@ -306,7 +306,7 @@ func (h *OpenAIAPIHandler) forwardResponsesAsChatStream(c *gin.Context, flusher
|
|||||||
WriteChunk: func(chunk []byte) {
|
WriteChunk: func(chunk []byte) {
|
||||||
outputs := codexconverter.ConvertCodexResponseToOpenAI(ctx, modelName, originalChatJSON, responsesRequestJSON, chunk, param)
|
outputs := codexconverter.ConvertCodexResponseToOpenAI(ctx, modelName, originalChatJSON, responsesRequestJSON, chunk, param)
|
||||||
for _, out := range outputs {
|
for _, out := range outputs {
|
||||||
if out == "" {
|
if len(out) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", out)
|
_, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", out)
|
||||||
@@ -345,23 +345,23 @@ func convertChatCompletionsResponseToCompletions(rawJSON []byte) []byte {
|
|||||||
root := gjson.ParseBytes(rawJSON)
|
root := gjson.ParseBytes(rawJSON)
|
||||||
|
|
||||||
// Base completions response structure
|
// 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
|
// Copy basic fields
|
||||||
if id := root.Get("id"); id.Exists() {
|
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() {
|
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() {
|
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() {
|
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
|
// Convert choices from chat completions to completions format
|
||||||
@@ -401,10 +401,10 @@ func convertChatCompletionsResponseToCompletions(rawJSON []byte) []byte {
|
|||||||
|
|
||||||
if len(choices) > 0 {
|
if len(choices) > 0 {
|
||||||
choicesJSON, _ := json.Marshal(choices)
|
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.
|
// convertChatCompletionsStreamChunkToCompletions converts a streaming chat completions chunk to completions format.
|
||||||
@@ -445,19 +445,19 @@ func convertChatCompletionsStreamChunkToCompletions(chunkData []byte) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Base completions stream response structure
|
// 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
|
// Copy basic fields
|
||||||
if id := root.Get("id"); id.Exists() {
|
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() {
|
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() {
|
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
|
// Convert choices from chat completions delta to completions format
|
||||||
@@ -496,15 +496,15 @@ func convertChatCompletionsStreamChunkToCompletions(chunkData []byte) []byte {
|
|||||||
|
|
||||||
if len(choices) > 0 {
|
if len(choices) > 0 {
|
||||||
choicesJSON, _ := json.Marshal(choices)
|
choicesJSON, _ := json.Marshal(choices)
|
||||||
out, _ = sjson.SetRaw(out, "choices", string(choicesJSON))
|
out, _ = sjson.SetRawBytes(out, "choices", choicesJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy usage if present
|
// Copy usage if present
|
||||||
if usage := root.Get("usage"); usage.Exists() {
|
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
|
// handleNonStreamingResponse handles non-streaming chat completion responses
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ func (h *OpenAIResponsesAPIHandler) handleNonStreamingResponseViaChat(c *gin.Con
|
|||||||
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
handlers.WriteUpstreamHeaders(c.Writer.Header(), upstreamHeaders)
|
||||||
var param any
|
var param any
|
||||||
converted := responsesconverter.ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(cliCtx, modelName, originalResponsesJSON, originalResponsesJSON, resp, ¶m)
|
converted := responsesconverter.ConvertOpenAIChatCompletionsResponseToOpenAIResponsesNonStream(cliCtx, modelName, originalResponsesJSON, originalResponsesJSON, resp, ¶m)
|
||||||
if converted == "" {
|
if len(converted) == 0 {
|
||||||
h.WriteErrorResponse(c, &interfaces.ErrorMessage{
|
h.WriteErrorResponse(c, &interfaces.ErrorMessage{
|
||||||
StatusCode: http.StatusInternalServerError,
|
StatusCode: http.StatusInternalServerError,
|
||||||
Error: fmt.Errorf("failed to convert chat completion response to responses format"),
|
Error: fmt.Errorf("failed to convert chat completion response to responses format"),
|
||||||
@@ -199,7 +199,7 @@ func (h *OpenAIResponsesAPIHandler) handleNonStreamingResponseViaChat(c *gin.Con
|
|||||||
cliCancel(fmt.Errorf("response conversion failed"))
|
cliCancel(fmt.Errorf("response conversion failed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = c.Writer.Write([]byte(converted))
|
_, _ = c.Writer.Write(converted)
|
||||||
cliCancel()
|
cliCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,13 +350,13 @@ func (h *OpenAIResponsesAPIHandler) handleStreamingResponseViaChat(c *gin.Contex
|
|||||||
func writeChatAsResponsesChunk(c *gin.Context, ctx context.Context, modelName string, originalResponsesJSON, chunk []byte, param *any) {
|
func writeChatAsResponsesChunk(c *gin.Context, ctx context.Context, modelName string, originalResponsesJSON, chunk []byte, param *any) {
|
||||||
outputs := responsesconverter.ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx, modelName, originalResponsesJSON, originalResponsesJSON, chunk, param)
|
outputs := responsesconverter.ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx, modelName, originalResponsesJSON, originalResponsesJSON, chunk, param)
|
||||||
for _, out := range outputs {
|
for _, out := range outputs {
|
||||||
if out == "" {
|
if len(out) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix([]byte(out), []byte("event:")) {
|
if bytes.HasPrefix(out, []byte("event:")) {
|
||||||
_, _ = c.Writer.Write([]byte("\n"))
|
_, _ = c.Writer.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
_, _ = c.Writer.Write([]byte(out))
|
_, _ = c.Writer.Write(out)
|
||||||
_, _ = c.Writer.Write([]byte("\n"))
|
_, _ = c.Writer.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,13 +366,13 @@ func (h *OpenAIResponsesAPIHandler) forwardChatAsResponsesStream(c *gin.Context,
|
|||||||
WriteChunk: func(chunk []byte) {
|
WriteChunk: func(chunk []byte) {
|
||||||
outputs := responsesconverter.ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx, modelName, originalResponsesJSON, originalResponsesJSON, chunk, param)
|
outputs := responsesconverter.ConvertOpenAIChatCompletionsResponseToOpenAIResponses(ctx, modelName, originalResponsesJSON, originalResponsesJSON, chunk, param)
|
||||||
for _, out := range outputs {
|
for _, out := range outputs {
|
||||||
if out == "" {
|
if len(out) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix([]byte(out), []byte("event:")) {
|
if bytes.HasPrefix(out, []byte("event:")) {
|
||||||
_, _ = c.Writer.Write([]byte("\n"))
|
_, _ = c.Writer.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
_, _ = c.Writer.Write([]byte(out))
|
_, _ = c.Writer.Write(out)
|
||||||
_, _ = c.Writer.Write([]byte("\n"))
|
_, _ = c.Writer.Write([]byte("\n"))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, o
|
|||||||
defer manualPromptTimer.Stop()
|
defer manualPromptTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manualInputCh <-chan string
|
||||||
|
var manualInputErrCh <-chan error
|
||||||
|
|
||||||
waitForCallback:
|
waitForCallback:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -115,10 +118,11 @@ waitForCallback:
|
|||||||
break waitForCallback
|
break waitForCallback
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ")
|
manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the antigravity callback URL (or press Enter to keep waiting): ")
|
||||||
if errPrompt != nil {
|
continue
|
||||||
return nil, errPrompt
|
case input := <-manualInputCh:
|
||||||
}
|
manualInputCh = nil
|
||||||
|
manualInputErrCh = nil
|
||||||
parsed, errParse := misc.ParseOAuthCallback(input)
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
if errParse != nil {
|
if errParse != nil {
|
||||||
return nil, errParse
|
return nil, errParse
|
||||||
@@ -132,6 +136,8 @@ waitForCallback:
|
|||||||
Error: parsed.Error,
|
Error: parsed.Error,
|
||||||
}
|
}
|
||||||
break waitForCallback
|
break waitForCallback
|
||||||
|
case errManual := <-manualInputErrCh:
|
||||||
|
return nil, errManual
|
||||||
case <-timeoutTimer.C:
|
case <-timeoutTimer.C:
|
||||||
return nil, fmt.Errorf("antigravity: authentication timed out")
|
return nil, fmt.Errorf("antigravity: authentication timed out")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
|||||||
defer manualPromptTimer.Stop()
|
defer manualPromptTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manualInputCh <-chan string
|
||||||
|
var manualInputErrCh <-chan error
|
||||||
|
|
||||||
waitForCallback:
|
waitForCallback:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -149,10 +152,11 @@ waitForCallback:
|
|||||||
return nil, err
|
return nil, err
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ")
|
manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Claude callback URL (or press Enter to keep waiting): ")
|
||||||
if errPrompt != nil {
|
continue
|
||||||
return nil, errPrompt
|
case input := <-manualInputCh:
|
||||||
}
|
manualInputCh = nil
|
||||||
|
manualInputErrCh = nil
|
||||||
parsed, errParse := misc.ParseOAuthCallback(input)
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
if errParse != nil {
|
if errParse != nil {
|
||||||
return nil, errParse
|
return nil, errParse
|
||||||
@@ -167,6 +171,8 @@ waitForCallback:
|
|||||||
Error: parsed.Error,
|
Error: parsed.Error,
|
||||||
}
|
}
|
||||||
break waitForCallback
|
break waitForCallback
|
||||||
|
case errManual := <-manualInputErrCh:
|
||||||
|
return nil, errManual
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
defer manualPromptTimer.Stop()
|
defer manualPromptTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manualInputCh <-chan string
|
||||||
|
var manualInputErrCh <-chan error
|
||||||
|
|
||||||
waitForCallback:
|
waitForCallback:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -152,10 +155,11 @@ waitForCallback:
|
|||||||
return nil, err
|
return nil, err
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ")
|
manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Codex callback URL (or press Enter to keep waiting): ")
|
||||||
if errPrompt != nil {
|
continue
|
||||||
return nil, errPrompt
|
case input := <-manualInputCh:
|
||||||
}
|
manualInputCh = nil
|
||||||
|
manualInputErrCh = nil
|
||||||
parsed, errParse := misc.ParseOAuthCallback(input)
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
if errParse != nil {
|
if errParse != nil {
|
||||||
return nil, errParse
|
return nil, errParse
|
||||||
@@ -170,6 +174,8 @@ waitForCallback:
|
|||||||
Error: parsed.Error,
|
Error: parsed.Error,
|
||||||
}
|
}
|
||||||
break waitForCallback
|
break waitForCallback
|
||||||
|
case errManual := <-manualInputErrCh:
|
||||||
|
return nil, errManual
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
defer manualPromptTimer.Stop()
|
defer manualPromptTimer.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manualInputCh <-chan string
|
||||||
|
var manualInputErrCh <-chan error
|
||||||
|
|
||||||
waitForCallback:
|
waitForCallback:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -128,10 +131,11 @@ waitForCallback:
|
|||||||
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
|
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ")
|
manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the iFlow callback URL (or press Enter to keep waiting): ")
|
||||||
if errPrompt != nil {
|
continue
|
||||||
return nil, errPrompt
|
case input := <-manualInputCh:
|
||||||
}
|
manualInputCh = nil
|
||||||
|
manualInputErrCh = nil
|
||||||
parsed, errParse := misc.ParseOAuthCallback(input)
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
if errParse != nil {
|
if errParse != nil {
|
||||||
return nil, errParse
|
return nil, errParse
|
||||||
@@ -145,6 +149,8 @@ waitForCallback:
|
|||||||
Error: parsed.Error,
|
Error: parsed.Error,
|
||||||
}
|
}
|
||||||
break waitForCallback
|
break waitForCallback
|
||||||
|
case errManual := <-manualInputErrCh:
|
||||||
|
return nil, errManual
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if result.Error != "" {
|
if result.Error != "" {
|
||||||
|
|||||||
@@ -13,16 +13,16 @@ func HasResponseTransformerByFormatName(from, to Format) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TranslateStreamByFormatName converts streaming responses between schemas by their string identifiers.
|
// 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)
|
return TranslateStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateNonStreamByFormatName converts non-streaming responses between schemas by their string identifiers.
|
// 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)
|
return TranslateNonStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateTokenCountByFormatName converts token counts between schemas by their string identifiers.
|
// 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)
|
return TranslateTokenCount(ctx, from, to, count, rawJSON)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type ResponseEnvelope struct {
|
|||||||
Model string
|
Model string
|
||||||
Stream bool
|
Stream bool
|
||||||
Body []byte
|
Body []byte
|
||||||
Chunks []string
|
Chunks [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestMiddleware decorates request translation.
|
// RequestMiddleware decorates request translation.
|
||||||
@@ -87,7 +87,7 @@ func (p *Pipeline) TranslateResponse(ctx context.Context, from, to Format, resp
|
|||||||
if input.Stream {
|
if input.Stream {
|
||||||
input.Chunks = p.registry.TranslateStream(ctx, from, to, input.Model, originalReq, translatedReq, input.Body, param)
|
input.Chunks = p.registry.TranslateStream(ctx, from, to, input.Model, originalReq, translatedReq, input.Body, param)
|
||||||
} else {
|
} 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
|
input.Format = to
|
||||||
return input, nil
|
return input, nil
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ func (r *Registry) HasResponseTransformer(from, to Format) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TranslateStream applies the registered streaming response translator.
|
// 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()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
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 fn.Stream(ctx, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []string{string(rawJSON)}
|
return [][]byte{rawJSON}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateNonStream applies the registered non-stream response translator.
|
// 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()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
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 fn.NonStream(ctx, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return string(rawJSON)
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateNonStream applies the registered non-stream response translator.
|
// TranslateTokenCount applies the registered token count response translator.
|
||||||
func (r *Registry) TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) string {
|
func (r *Registry) TranslateTokenCount(ctx context.Context, from, to Format, count int64, rawJSON []byte) []byte {
|
||||||
r.mu.RLock()
|
r.mu.RLock()
|
||||||
defer r.mu.RUnlock()
|
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 fn.TokenCount(ctx, count)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return string(rawJSON)
|
return rawJSON
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultRegistry = NewRegistry()
|
var defaultRegistry = NewRegistry()
|
||||||
@@ -127,16 +127,16 @@ func HasResponseTransformer(from, to Format) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TranslateStream is a helper on the default registry.
|
// 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)
|
return defaultRegistry.TranslateStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateNonStream is a helper on the default registry.
|
// 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)
|
return defaultRegistry.TranslateNonStream(ctx, from, to, model, originalRequestRawJSON, requestRawJSON, rawJSON, param)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TranslateTokenCount is a helper on the default registry.
|
// 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)
|
return defaultRegistry.TranslateTokenCount(ctx, from, to, count, rawJSON)
|
||||||
}
|
}
|
||||||
|
|||||||
52
sdk/translator/registry_bytes_test.go
Normal file
52
sdk/translator/registry_bytes_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.
|
// 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 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.
|
// 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) []string
|
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.
|
// 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 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.
|
// It returns the converted response as a single byte slice.
|
||||||
type ResponseNonStreamTransform func(ctx context.Context, model string, originalRequestRawJSON, requestRawJSON, rawJSON []byte, param *any) string
|
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.
|
// 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.
|
// 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) string
|
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,
|
// ResponseTransform is a struct that groups together the functions for transforming streaming and non-streaming responses,
|
||||||
// as well as token counts.
|
// as well as token counts.
|
||||||
|
|||||||
Reference in New Issue
Block a user