Skip to content

Commit 3e20b00

Browse files
authored
Merge pull request router-for-me#163 from router-for-me/nb
fix(gemini): map responseModalities to uppercase IMAGE/TEXT
2 parents 5da5674 + e370f86 commit 3e20b00

File tree

6 files changed

+63
-15
lines changed

6 files changed

+63
-15
lines changed

internal/runtime/executor/aistudio_executor.go

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package executor
33
import (
44
"bytes"
55
"context"
6+
"encoding/json"
67
"fmt"
78
"net/http"
89
"net/url"
@@ -72,7 +73,7 @@ func (e *AistudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
7273
AuthValue: authValue,
7374
})
7475

75-
wsResp, err := e.relay.RoundTrip(ctx, e.provider, wsReq)
76+
wsResp, err := e.relay.NonStream(ctx, e.provider, wsReq)
7677
if err != nil {
7778
recordAPIResponseError(ctx, e.cfg, err)
7879
return resp, err
@@ -87,7 +88,7 @@ func (e *AistudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
8788
reporter.publish(ctx, parseGeminiUsage(wsResp.Body))
8889
var param any
8990
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, bytes.Clone(opts.OriginalRequest), bytes.Clone(translatedReq), bytes.Clone(wsResp.Body), &param)
90-
resp = cliproxyexecutor.Response{Payload: []byte(out)}
91+
resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON([]byte(out))}
9192
return resp, nil
9293
}
9394

@@ -156,7 +157,7 @@ func (e *AistudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
156157
}
157158
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, bytes.Clone(opts.OriginalRequest), translatedReq, bytes.Clone(filtered), &param)
158159
for i := range lines {
159-
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
160+
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
160161
}
161162
break
162163
}
@@ -172,7 +173,7 @@ func (e *AistudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
172173
}
173174
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, bytes.Clone(opts.OriginalRequest), translatedReq, bytes.Clone(event.Payload), &param)
174175
for i := range lines {
175-
out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}
176+
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON([]byte(lines[i]))}
176177
}
177178
reporter.publish(ctx, parseGeminiUsage(event.Payload))
178179
return
@@ -220,7 +221,7 @@ func (e *AistudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
220221
AuthType: authType,
221222
AuthValue: authValue,
222223
})
223-
resp, err := e.relay.RoundTrip(ctx, e.provider, wsReq)
224+
resp, err := e.relay.NonStream(ctx, e.provider, wsReq)
224225
if err != nil {
225226
recordAPIResponseError(ctx, e.cfg, err)
226227
return cliproxyexecutor.Response{}, err
@@ -346,3 +347,50 @@ func stripUsageMetadataFromJSON(rawJSON []byte) ([]byte, bool) {
346347
}
347348
return cleaned, true
348349
}
350+
351+
// ensureColonSpacedJSON normalizes JSON objects so that colons are followed by a single space while
352+
// keeping the payload otherwise compact. Non-JSON inputs are returned unchanged.
353+
func ensureColonSpacedJSON(payload []byte) []byte {
354+
trimmed := bytes.TrimSpace(payload)
355+
if len(trimmed) == 0 {
356+
return payload
357+
}
358+
359+
var decoded any
360+
if err := json.Unmarshal(trimmed, &decoded); err != nil {
361+
return payload
362+
}
363+
364+
indented, err := json.MarshalIndent(decoded, "", " ")
365+
if err != nil {
366+
return payload
367+
}
368+
369+
compacted := make([]byte, 0, len(indented))
370+
inString := false
371+
skipSpace := false
372+
373+
for i := 0; i < len(indented); i++ {
374+
ch := indented[i]
375+
if ch == '"' && (i == 0 || indented[i-1] != '\\') {
376+
inString = !inString
377+
}
378+
379+
if !inString {
380+
if ch == '\n' || ch == '\r' {
381+
skipSpace = true
382+
continue
383+
}
384+
if skipSpace {
385+
if ch == ' ' || ch == '\t' {
386+
continue
387+
}
388+
skipSpace = false
389+
}
390+
}
391+
392+
compacted = append(compacted, ch)
393+
}
394+
395+
return compacted
396+
}

internal/runtime/executor/gemini_cli_executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,7 @@ func fixGeminiCLIImageAspectRatio(modelName string, rawJSON []byte) []byte {
703703
}
704704

705705
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.contents.0.parts", []byte(newPartsJson))
706-
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.generationConfig.responseModalities", []byte(`["Image", "Text"]`))
706+
rawJSON, _ = sjson.SetRawBytes(rawJSON, "request.generationConfig.responseModalities", []byte(`["IMAGE", "TEXT"]`))
707707
}
708708
}
709709
rawJSON, _ = sjson.DeleteBytes(rawJSON, "request.generationConfig.imageConfig")

internal/runtime/executor/gemini_executor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ func fixGeminiImageAspectRatio(modelName string, rawJSON []byte) []byte {
494494
}
495495

496496
rawJSON, _ = sjson.SetRawBytes(rawJSON, "contents.0.parts", []byte(newPartsJson))
497-
rawJSON, _ = sjson.SetRawBytes(rawJSON, "generationConfig.responseModalities", []byte(`["Image", "Text"]`))
497+
rawJSON, _ = sjson.SetRawBytes(rawJSON, "generationConfig.responseModalities", []byte(`["IMAGE", "TEXT"]`))
498498
}
499499
}
500500
rawJSON, _ = sjson.DeleteBytes(rawJSON, "generationConfig.imageConfig")

internal/translator/gemini-cli/openai/chat-completions/gemini-cli_openai_request.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ func ConvertOpenAIRequestToGeminiCLI(modelName string, inputRawJSON []byte, _ bo
6666
}
6767

6868
// Map OpenAI modalities -> Gemini CLI request.generationConfig.responseModalities
69-
// e.g. "modalities": ["image", "text"] -> ["Image", "Text"]
69+
// e.g. "modalities": ["image", "text"] -> ["IMAGE", "TEXT"]
7070
if mods := gjson.GetBytes(rawJSON, "modalities"); mods.Exists() && mods.IsArray() {
7171
var responseMods []string
7272
for _, m := range mods.Array() {
7373
switch strings.ToLower(m.String()) {
7474
case "text":
75-
responseMods = append(responseMods, "Text")
75+
responseMods = append(responseMods, "TEXT")
7676
case "image":
77-
responseMods = append(responseMods, "Image")
77+
responseMods = append(responseMods, "IMAGE")
7878
}
7979
}
8080
if len(responseMods) > 0 {

internal/translator/gemini/openai/chat-completions/gemini_openai_request.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ func ConvertOpenAIRequestToGemini(modelName string, inputRawJSON []byte, _ bool)
6666
}
6767

6868
// Map OpenAI modalities -> Gemini generationConfig.responseModalities
69-
// e.g. "modalities": ["image", "text"] -> ["Image", "Text"]
69+
// e.g. "modalities": ["image", "text"] -> ["IMAGE", "TEXT"]
7070
if mods := gjson.GetBytes(rawJSON, "modalities"); mods.Exists() && mods.IsArray() {
7171
var responseMods []string
7272
for _, m := range mods.Array() {
7373
switch strings.ToLower(m.String()) {
7474
case "text":
75-
responseMods = append(responseMods, "Text")
75+
responseMods = append(responseMods, "TEXT")
7676
case "image":
77-
responseMods = append(responseMods, "Image")
77+
responseMods = append(responseMods, "IMAGE")
7878
}
7979
}
8080
if len(responseMods) > 0 {

internal/wsrelay/http.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ type StreamEvent struct {
3535
Err error
3636
}
3737

38-
// RoundTrip executes a non-streaming HTTP request using the websocket provider.
39-
func (m *Manager) RoundTrip(ctx context.Context, provider string, req *HTTPRequest) (*HTTPResponse, error) {
38+
// NonStream executes a non-streaming HTTP request using the websocket provider.
39+
func (m *Manager) NonStream(ctx context.Context, provider string, req *HTTPRequest) (*HTTPResponse, error) {
4040
if req == nil {
4141
return nil, fmt.Errorf("wsrelay: request is nil")
4242
}

0 commit comments

Comments
 (0)