package executor import ( "bytes" "context" "io" "net/http" "net/http/httptest" "testing" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson") func TestCapGeminiMaxOutputTokensUsesOutputTokenLimit(t *testing.T) { body := []byte(`{"generationConfig":{"maxOutputTokens":500000,"temperature":0.2},"contents":[]}`) out := capGeminiMaxOutputTokens(body, "gemini-3.1-pro-preview") if got := gjson.GetBytes(out, "generationConfig.maxOutputTokens").Int(); got != 65536 { t.Fatalf("maxOutputTokens = %d, want 65536", got) } if got := gjson.GetBytes(out, "generationConfig.temperature").Float(); got != 0.2 { t.Fatalf("temperature = %v, want 0.2", got) }} func TestCapGeminiMaxOutputTokensLeavesAllowedOrUnknown(t *testing.T) { tests := []struct { name string model string body []byte want int64 }{ { name: "allowed value", model: "gemini-3.1-pro-preview", body: []byte(`{"generationConfig":{"maxOutputTokens":64000}}`), want: 64000, }, { name: "unknown model", model: "custom-gemini-model", body: []byte(`{"generationConfig":{"maxOutputTokens":500000}}`), want: 500000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { out := capGeminiMaxOutputTokens(tt.body, tt.model) if got := gjson.GetBytes(out, "generationConfig.maxOutputTokens").Int(); got != tt.want { t.Fatalf("maxOutputTokens = %d, want %d", got, tt.want) } }) }} func TestGeminiExecutorExecuteCapsMaxOutputTokensBeforeUpstream(t *testing.T) { var upstreamMaxOutputTokens int64 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("read request body: %v", err) } upstreamMaxOutputTokens = gjson.GetBytes(body, "generationConfig.maxOutputTokens").Int() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() exec := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} req := cliproxyexecutor.Request{ Model: "gemini-3.1-pro-preview", Payload: []byte(`{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"maxOutputTokens":500000}}`), } if _, err := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); err != nil { t.Fatalf("Execute() error = %v", err) } if upstreamMaxOutputTokens != 65536 { t.Fatalf("upstream maxOutputTokens = %d, want 65536", upstreamMaxOutputTokens) }} func TestGeminiExecutorExecutePrependsLeadingUser(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[` + `{"role":"model","parts":[{"functionCall":{"name":"lookup","args":{"key":"value"}}}]},` + `{"role":"user","parts":[{"functionResponse":{"name":"lookup","response":{"result":"ok"}}}]}` + `]}`), } if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 3 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" || contents[2].Get("role").String() != "user" { t.Fatalf("upstream roles malformed: %s", upstreamBody) } if got := contents[0].Get("parts.0.text").String(); got != "" { t.Fatalf("leading user prompt = %q, want empty string; body=%s", got, upstreamBody) }} func TestGeminiExecutorExecutePrependsLeadingUserForIssue4959ResponsesHistory(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} if _, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: issue4959ResponsesModelFirstPayload(), }, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAIResponse}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } assertIssue4959LeadingUserContents(t, gjson.GetBytes(upstreamBody, "contents").Array())} func TestGeminiExecutorExecuteAppendsTrailingUserForTrailingModelTurn(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[` + `{"role":"user","parts":[{"text":"hello"}]},` + `{"role":"model","parts":[{"text":"answer"}]}` + `]}`), } if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 3 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" || contents[2].Get("role").String() != "user" { t.Fatalf("upstream roles malformed: %s", upstreamBody) } if got := contents[2].Get("parts.0.text").String(); got != "" { t.Fatalf("trailing user prompt = %q, want empty string; body=%s", got, upstreamBody) }} func TestGeminiExecutorCountTokensPrependsLeadingUser(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"totalTokens":7}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[{"role":"model","parts":[{"text":"prior output"}]}]}`), } ctx := cliproxyexecutor.WithUpstreamAttemptTracker(context.Background()) if _, errCount := executor.CountTokens(ctx, auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errCount != nil { t.Fatalf("CountTokens() error = %v", errCount) } if !cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("CountTokens() did not mark the HTTP request as an upstream attempt") } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 2 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" { t.Fatalf("countTokens roles malformed: %s", upstreamBody) } if text := contents[0].Get("parts.0.text"); !text.Exists() || text.String() != "" { t.Fatalf("countTokens synthetic user missing: %s", upstreamBody) } if got := contents[1].Get("parts.0.text").String(); got != "prior output" { t.Fatalf("countTokens model text = %q, want prior output; body=%s", got, upstreamBody) } request.Metadata = map[string]any{"action": "countTokens"} if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute(countTokens) error = %v", errExecute) } contents = gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 2 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" { t.Fatalf("Execute(countTokens) roles malformed: %s", upstreamBody) }} func TestGeminiExecutorAppliesPayloadRulesBeforeLeadingUserNormalization(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}]}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{ Models: []config.PayloadModelRule{{Name: "gemini-3.7-flash", Protocol: "gemini"}}, Params: map[string]any{"contents.0.parts.0.text": "payload override"}, }}}}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[` + `{"role":"model","parts":[{"text":"prior output"}]},` + `{"role":"user","parts":[{"text":"continue"}]}` + `]}`), } if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 3 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" { t.Fatalf("upstream roles malformed: %s", upstreamBody) } if text := contents[0].Get("parts.0.text"); !text.Exists() || text.String() != "" { t.Fatalf("synthetic leading user changed: %s", upstreamBody) } if got := contents[1].Get("parts.0.text").String(); got != "payload override" { t.Fatalf("payload rule applied to %q, want original first model turn; body=%s", got, upstreamBody) }} func TestGeminiExecutorInteractionsWithGeminiAPIKeyUsesGeminiEndpoint(t *testing.T) { var gotPath string var gotRevision string var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotRevision = r.Header.Get("Api-Revision") body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() exec := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "gemini", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "gemini-3.5-flash", Payload: []byte(`{"model":"gemini-3.5-flash","input":"hi"}`), } _, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatInteractions, ResponseFormat: sdktranslator.FormatInteractions, }) if errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } if gotPath != "/v1beta/models/gemini-3.5-flash:generateContent" { t.Fatalf("path = %q, want Gemini generateContent endpoint", gotPath) } if gotRevision != "" { t.Fatalf("Api-Revision = %q, want empty for Gemini protocol request", gotRevision) } if !gjson.GetBytes(upstreamBody, "contents.0.parts.0.text").Exists() { t.Fatalf("contents text missing from translated Gemini body: %s", string(upstreamBody)) } if gjson.GetBytes(upstreamBody, "input").Exists() { t.Fatalf("raw interactions input exists in translated Gemini body: %s", string(upstreamBody)) }} func TestGeminiExecutorNativeInteractionsUsesInteractionsEndpoint(t *testing.T) { var gotPath string var gotRevision string var gotModelExists bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotRevision = r.Header.Get("Api-Revision") body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } gotModelExists = gjson.GetBytes(body, "model").Exists() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`)) })) defer server.Close() exec := NewGeminiInteractionsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "gemini-interactions", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "agents/test-agent", Payload: []byte(`{"agent":"agents/test-agent","input":"hi"}`), } resp, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatInteractions, ResponseFormat: sdktranslator.FormatInteractions, }) if errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } if gotPath != "/v1beta/interactions" { t.Fatalf("path = %q, want /v1beta/interactions", gotPath) } if gotRevision != "2026-05-20" { t.Fatalf("Api-Revision = %q, want 2026-05-20", gotRevision) } if gotModelExists { t.Fatal("model field exists for agent-only request, want absent") } if got := gjson.GetBytes(resp.Payload, "id").String(); got != "interaction_1" { t.Fatalf("response id = %q, want interaction_1", got) }} func TestGeminiExecutorNativeInteractionsTranslatesOpenAIResponsesRequest(t *testing.T) { var gotPath string var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`)) })) defer server.Close() exec := NewGeminiInteractionsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "gemini-interactions", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "gemini-3.1-flash-lite", Payload: []byte(`{ "model":"gemini-3.1-flash-lite", "instructions":"be brief", "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}], "reasoning":{"effort":"high","summary":"auto"} }`), } resp, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, }) if errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } if gotPath != "/v1beta/interactions" { t.Fatalf("path = %q, want /v1beta/interactions", gotPath) } if got := gjson.GetBytes(upstreamBody, "input.0.type").String(); got != "user_input" { t.Fatalf("input.0.type = %q, want user_input. Body: %s", got, string(upstreamBody)) } if got := gjson.GetBytes(upstreamBody, "generation_config.thinking_level").String(); got != "high" { t.Fatalf("thinking_level = %q, want high. Body: %s", got, string(upstreamBody)) } if got := gjson.GetBytes(resp.Payload, "output.0.content.0.text").String(); got != "ok" { t.Fatalf("response text = %q, want ok. Payload: %s", got, string(resp.Payload)) }} func TestGeminiExecutorNativeInteractionsPayloadRulesUseResponsesFromProtocol(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}]}`)) })) defer server.Close() exec := NewGeminiInteractionsExecutor(&config.Config{ Payload: config.PayloadConfig{ Override: []config.PayloadRule{ { Models: []config.PayloadModelRule{ {Name: "gemini-3.1-flash-lite", Protocol: "interactions", FromProtocol: "openai"}, }, Params: map[string]any{ "generation_config.thinking_summaries": "wrong", }, }, { Models: []config.PayloadModelRule{ {Name: "gemini-3.1-flash-lite", Protocol: "interactions", FromProtocol: "responses"}, }, Params: map[string]any{ "generation_config.thinking_summaries": "detailed", }, }, }, }, }) auth := &cliproxyauth.Auth{ Provider: "gemini-interactions", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "gemini-3.1-flash-lite", Payload: []byte(`{ "model":"gemini-3.1-flash-lite", "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}] }`), } _, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse,Lines 1–500 of 1382. Load more
package executor import ( "bytes" "context" "io" "net/http" "net/http/httptest" "testing" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" _ "github.com/router-for-me/CLIProxyAPI/v7/internal/translator" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v7/sdk/translator" "github.com/tidwall/gjson" ) func TestCapGeminiMaxOutputTokensUsesOutputTokenLimit(t *testing.T) { body := []byte(`{"generationConfig":{"maxOutputTokens":500000,"temperature":0.2},"contents":[]}`) out := capGeminiMaxOutputTokens(body, "gemini-3.1-pro-preview") if got := gjson.GetBytes(out, "generationConfig.maxOutputTokens").Int(); got != 65536 { t.Fatalf("maxOutputTokens = %d, want 65536", got) } if got := gjson.GetBytes(out, "generationConfig.temperature").Float(); got != 0.2 { t.Fatalf("temperature = %v, want 0.2", got) } } func TestCapGeminiMaxOutputTokensLeavesAllowedOrUnknown(t *testing.T) { tests := []struct { name string model string body []byte want int64 }{ { name: "allowed value", model: "gemini-3.1-pro-preview", body: []byte(`{"generationConfig":{"maxOutputTokens":64000}}`), want: 64000, }, { name: "unknown model", model: "custom-gemini-model", body: []byte(`{"generationConfig":{"maxOutputTokens":500000}}`), want: 500000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { out := capGeminiMaxOutputTokens(tt.body, tt.model) if got := gjson.GetBytes(out, "generationConfig.maxOutputTokens").Int(); got != tt.want { t.Fatalf("maxOutputTokens = %d, want %d", got, tt.want) } }) } } func TestGeminiExecutorExecuteCapsMaxOutputTokensBeforeUpstream(t *testing.T) { var upstreamMaxOutputTokens int64 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("read request body: %v", err) } upstreamMaxOutputTokens = gjson.GetBytes(body, "generationConfig.maxOutputTokens").Int() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() exec := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} req := cliproxyexecutor.Request{ Model: "gemini-3.1-pro-preview", Payload: []byte(`{"contents":[{"role":"user","parts":[{"text":"hi"}]}],"generationConfig":{"maxOutputTokens":500000}}`), } if _, err := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); err != nil { t.Fatalf("Execute() error = %v", err) } if upstreamMaxOutputTokens != 65536 { t.Fatalf("upstream maxOutputTokens = %d, want 65536", upstreamMaxOutputTokens) } } func TestGeminiExecutorExecutePrependsLeadingUser(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[` + `{"role":"model","parts":[{"functionCall":{"name":"lookup","args":{"key":"value"}}}]},` + `{"role":"user","parts":[{"functionResponse":{"name":"lookup","response":{"result":"ok"}}}]}` + `]}`), } if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 3 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" || contents[2].Get("role").String() != "user" { t.Fatalf("upstream roles malformed: %s", upstreamBody) } if got := contents[0].Get("parts.0.text").String(); got != "" { t.Fatalf("leading user prompt = %q, want empty string; body=%s", got, upstreamBody) } } func TestGeminiExecutorExecutePrependsLeadingUserForIssue4959ResponsesHistory(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} if _, errExecute := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: issue4959ResponsesModelFirstPayload(), }, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatOpenAIResponse}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } assertIssue4959LeadingUserContents(t, gjson.GetBytes(upstreamBody, "contents").Array()) } func TestGeminiExecutorExecuteAppendsTrailingUserForTrailingModelTurn(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[` + `{"role":"user","parts":[{"text":"hello"}]},` + `{"role":"model","parts":[{"text":"answer"}]}` + `]}`), } if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 3 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" || contents[2].Get("role").String() != "user" { t.Fatalf("upstream roles malformed: %s", upstreamBody) } if got := contents[2].Get("parts.0.text").String(); got != "" { t.Fatalf("trailing user prompt = %q, want empty string; body=%s", got, upstreamBody) } } func TestGeminiExecutorCountTokensPrependsLeadingUser(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"totalTokens":7}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[{"role":"model","parts":[{"text":"prior output"}]}]}`), } ctx := cliproxyexecutor.WithUpstreamAttemptTracker(context.Background()) if _, errCount := executor.CountTokens(ctx, auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errCount != nil { t.Fatalf("CountTokens() error = %v", errCount) } if !cliproxyexecutor.UpstreamAttempted(ctx) { t.Fatal("CountTokens() did not mark the HTTP request as an upstream attempt") } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 2 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" { t.Fatalf("countTokens roles malformed: %s", upstreamBody) } if text := contents[0].Get("parts.0.text"); !text.Exists() || text.String() != "" { t.Fatalf("countTokens synthetic user missing: %s", upstreamBody) } if got := contents[1].Get("parts.0.text").String(); got != "prior output" { t.Fatalf("countTokens model text = %q, want prior output; body=%s", got, upstreamBody) } request.Metadata = map[string]any{"action": "countTokens"} if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute(countTokens) error = %v", errExecute) } contents = gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 2 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" { t.Fatalf("Execute(countTokens) roles malformed: %s", upstreamBody) } } func TestGeminiExecutorAppliesPayloadRulesBeforeLeadingUserNormalization(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = append([]byte(nil), body...) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}]}`)) })) defer server.Close() executor := NewGeminiExecutor(&config.Config{Payload: config.PayloadConfig{Override: []config.PayloadRule{{ Models: []config.PayloadModelRule{{Name: "gemini-3.7-flash", Protocol: "gemini"}}, Params: map[string]any{"contents.0.parts.0.text": "payload override"}, }}}}) auth := &cliproxyauth.Auth{Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }} request := cliproxyexecutor.Request{ Model: "gemini-3.7-flash", Payload: []byte(`{"contents":[` + `{"role":"model","parts":[{"text":"prior output"}]},` + `{"role":"user","parts":[{"text":"continue"}]}` + `]}`), } if _, errExecute := executor.Execute(context.Background(), auth, request, cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatGemini}); errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } contents := gjson.GetBytes(upstreamBody, "contents").Array() if len(contents) != 3 || contents[0].Get("role").String() != "user" || contents[1].Get("role").String() != "model" { t.Fatalf("upstream roles malformed: %s", upstreamBody) } if text := contents[0].Get("parts.0.text"); !text.Exists() || text.String() != "" { t.Fatalf("synthetic leading user changed: %s", upstreamBody) } if got := contents[1].Get("parts.0.text").String(); got != "payload override" { t.Fatalf("payload rule applied to %q, want original first model turn; body=%s", got, upstreamBody) } } func TestGeminiExecutorInteractionsWithGeminiAPIKeyUsesGeminiEndpoint(t *testing.T) { var gotPath string var gotRevision string var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotRevision = r.Header.Get("Api-Revision") body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}`)) })) defer server.Close() exec := NewGeminiExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "gemini", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "gemini-3.5-flash", Payload: []byte(`{"model":"gemini-3.5-flash","input":"hi"}`), } _, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatInteractions, ResponseFormat: sdktranslator.FormatInteractions, }) if errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } if gotPath != "/v1beta/models/gemini-3.5-flash:generateContent" { t.Fatalf("path = %q, want Gemini generateContent endpoint", gotPath) } if gotRevision != "" { t.Fatalf("Api-Revision = %q, want empty for Gemini protocol request", gotRevision) } if !gjson.GetBytes(upstreamBody, "contents.0.parts.0.text").Exists() { t.Fatalf("contents text missing from translated Gemini body: %s", string(upstreamBody)) } if gjson.GetBytes(upstreamBody, "input").Exists() { t.Fatalf("raw interactions input exists in translated Gemini body: %s", string(upstreamBody)) } } func TestGeminiExecutorNativeInteractionsUsesInteractionsEndpoint(t *testing.T) { var gotPath string var gotRevision string var gotModelExists bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path gotRevision = r.Header.Get("Api-Revision") body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } gotModelExists = gjson.GetBytes(body, "model").Exists() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`)) })) defer server.Close() exec := NewGeminiInteractionsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "gemini-interactions", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "agents/test-agent", Payload: []byte(`{"agent":"agents/test-agent","input":"hi"}`), } resp, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatInteractions, ResponseFormat: sdktranslator.FormatInteractions, }) if errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } if gotPath != "/v1beta/interactions" { t.Fatalf("path = %q, want /v1beta/interactions", gotPath) } if gotRevision != "2026-05-20" { t.Fatalf("Api-Revision = %q, want 2026-05-20", gotRevision) } if gotModelExists { t.Fatal("model field exists for agent-only request, want absent") } if got := gjson.GetBytes(resp.Payload, "id").String(); got != "interaction_1" { t.Fatalf("response id = %q, want interaction_1", got) } } func TestGeminiExecutorNativeInteractionsTranslatesOpenAIResponsesRequest(t *testing.T) { var gotPath string var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}],"usage":{"input_tokens":1,"output_tokens":1,"total_tokens":2}}`)) })) defer server.Close() exec := NewGeminiInteractionsExecutor(&config.Config{}) auth := &cliproxyauth.Auth{ Provider: "gemini-interactions", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "gemini-3.1-flash-lite", Payload: []byte(`{ "model":"gemini-3.1-flash-lite", "instructions":"be brief", "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}], "reasoning":{"effort":"high","summary":"auto"} }`), } resp, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse, }) if errExecute != nil { t.Fatalf("Execute() error = %v", errExecute) } if gotPath != "/v1beta/interactions" { t.Fatalf("path = %q, want /v1beta/interactions", gotPath) } if got := gjson.GetBytes(upstreamBody, "input.0.type").String(); got != "user_input" { t.Fatalf("input.0.type = %q, want user_input. Body: %s", got, string(upstreamBody)) } if got := gjson.GetBytes(upstreamBody, "generation_config.thinking_level").String(); got != "high" { t.Fatalf("thinking_level = %q, want high. Body: %s", got, string(upstreamBody)) } if got := gjson.GetBytes(resp.Payload, "output.0.content.0.text").String(); got != "ok" { t.Fatalf("response text = %q, want ok. Payload: %s", got, string(resp.Payload)) } } func TestGeminiExecutorNativeInteractionsPayloadRulesUseResponsesFromProtocol(t *testing.T) { var upstreamBody []byte server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, errRead := io.ReadAll(r.Body) if errRead != nil { t.Fatalf("read request body: %v", errRead) } upstreamBody = body w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"id":"interaction_1","object":"interaction","status":"completed","steps":[{"type":"model_output","content":[{"text":"ok"}]}]}`)) })) defer server.Close() exec := NewGeminiInteractionsExecutor(&config.Config{ Payload: config.PayloadConfig{ Override: []config.PayloadRule{ { Models: []config.PayloadModelRule{ {Name: "gemini-3.1-flash-lite", Protocol: "interactions", FromProtocol: "openai"}, }, Params: map[string]any{ "generation_config.thinking_summaries": "wrong", }, }, { Models: []config.PayloadModelRule{ {Name: "gemini-3.1-flash-lite", Protocol: "interactions", FromProtocol: "responses"}, }, Params: map[string]any{ "generation_config.thinking_summaries": "detailed", }, }, }, }, }) auth := &cliproxyauth.Auth{ Provider: "gemini-interactions", Attributes: map[string]string{ "api_key": "test-key", "base_url": server.URL, }, } req := cliproxyexecutor.Request{ Model: "gemini-3.1-flash-lite", Payload: []byte(`{ "model":"gemini-3.1-flash-lite", "input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hi"}]}] }`), } _, errExecute := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatOpenAIResponse, ResponseFormat: sdktranslator.FormatOpenAIResponse,