diff --git a/llms/googleai/googleai_test.go b/llms/googleai/googleai_test.go index f8d90098d..7ea3f14f2 100644 --- a/llms/googleai/googleai_test.go +++ b/llms/googleai/googleai_test.go @@ -168,6 +168,35 @@ func TestGoogleAIGenerateContentWithSystemMessage(t *testing.T) { assert.NotEmpty(t, resp.Choices) } +func TestGoogleAIGenerateContentWithCustomBaseURL(t *testing.T) { + // This proves the option is accepted and doesn't break the SDK's path construction. + officialBaseURL := "https://generativelanguage.googleapis.com" + + llm := newHTTPRRClient(t, WithBaseURL(officialBaseURL)) + + ctx := context.Background() + content := []llms.MessageContent{ + { + Role: llms.ChatMessageTypeHuman, + Parts: []llms.ContentPart{ + llms.TextPart("Hey! How are you doing?"), + }, + }, + } + + resp, err := llm.GenerateContent(ctx, content) + if err != nil { + // Check if this is a recording mismatch error + if strings.Contains(err.Error(), "cached HTTP response not found") { + t.Skip("Recording format has changed or is incompatible. Hint: Re-run tests with -httprecord=. to record new HTTP interactions") + } + require.NoError(t, err) + } + + require.NoError(t, err) + require.NotNil(t, resp) +} + func TestGoogleAICall(t *testing.T) { llm := newHTTPRRClient(t) diff --git a/llms/googleai/googleai_unit_test.go b/llms/googleai/googleai_unit_test.go index ba2186551..d337e9918 100644 --- a/llms/googleai/googleai_unit_test.go +++ b/llms/googleai/googleai_unit_test.go @@ -138,6 +138,12 @@ func TestOptions(t *testing.T) { //nolint:funlen // comprehensive test //nolint: assert.Len(t, opts.ClientOptions, 1) }) + t.Run("WithBaseURL", func(t *testing.T) { + opts := &Options{} + WithBaseURL("https://custom.endpoint.com")(opts) + assert.Len(t, opts.ClientOptions, 1) + }) + t.Run("WithHTTPClient", func(t *testing.T) { opts := &Options{} WithHTTPClient(nil)(opts) diff --git a/llms/googleai/option.go b/llms/googleai/option.go index 6bd41cb42..cfbdfb8de 100644 --- a/llms/googleai/option.go +++ b/llms/googleai/option.go @@ -92,6 +92,16 @@ func WithRest() Option { } } +// WithBaseURL appends a ClientOption that configures the underlying Google AI +// client to use a custom base URL (endpoint). This is useful for directing +// requests to proxy servers, local development environments, or alternative +// regional endpoints not automatically handled by the default client setup. +func WithBaseURL(baseURL string) Option { + return func(opts *Options) { + opts.ClientOptions = append(opts.ClientOptions, option.WithEndpoint(baseURL)) + } +} + // WithHTTPClient append a ClientOption that uses the provided HTTP client to // make requests. // This is useful for vertex clients. diff --git a/llms/googleai/testdata/TestGoogleAIGenerateContentWithCustomBaseURL.httprr b/llms/googleai/testdata/TestGoogleAIGenerateContentWithCustomBaseURL.httprr new file mode 100644 index 000000000..390a8a019 --- /dev/null +++ b/llms/googleai/testdata/TestGoogleAIGenerateContentWithCustomBaseURL.httprr @@ -0,0 +1,77 @@ +httprr trace v1 +781 1535 +POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?%24alt=json%3Benum-encoding%3Dint HTTP/1.1 +Host: generativelanguage.googleapis.com +User-Agent: langchaingo-httprr +Content-Length: 362 +Content-Type: application/json +x-goog-api-client: gl-go/X.XX.X gccl/vX.XX.X genai-go/X.XX.X gapic/X.X.X gax/X.XX.X rest/UNKNOWN +x-goog-request-params: model=models%2Fgemini-2.0-flash + +{"model":"models/gemini-2.0-flash", "contents":[{"parts":[{"text":"Hey! How are you doing?"}], "role":"user"}], "safetySettings":[{"category":10, "threshold":3}, {"category":7, "threshold":3}, {"category":8, "threshold":3}, {"category":9, "threshold":3}], "generationConfig":{"candidateCount":1, "maxOutputTokens":2048, "temperature":0.5, "topP":0.95, "topK":3}}HTTP/2.0 200 OK +Content-Length: 1156 +Alt-Svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 +Content-Type: application/json; charset=UTF-8 +Date: Sat, 21 Feb 2026 09:05:56 GMT +Server: scaffolding on HTTPServer2 +Server-Timing: gfet4t7; dur=710 +Vary: Origin +Vary: X-Origin +Vary: Referer +X-Content-Type-Options: nosniff +X-Frame-Options: SAMEORIGIN +X-Xss-Protection: 0 + +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "I'm doing well, thank you for asking! As a large language model, I don't experience emotions or feelings like humans do, but I'm functioning optimally and ready to assist you. How can I help you today?\n" + } + ], + "role": "model" + }, + "finishReason": 1, + "safetyRatings": [ + { + "category": 8, + "probability": 1 + }, + { + "category": 10, + "probability": 1 + }, + { + "category": 7, + "probability": 1 + }, + { + "category": 9, + "probability": 1 + } + ], + "avgLogprobs": -0.0772957655848289 + } + ], + "usageMetadata": { + "promptTokenCount": 7, + "candidatesTokenCount": 49, + "totalTokenCount": 56, + "promptTokensDetails": [ + { + "modality": 1, + "tokenCount": 7 + } + ], + "candidatesTokensDetails": [ + { + "modality": 1, + "tokenCount": 49 + } + ] + }, + "modelVersion": "gemini-2.0-flash", + "responseId": "c3WZad6YHraWxN8PgovYIQ" +}