1382 lines · 55.2 KB · Go
RawDownload
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.