Skip to content

Commit ede87c2

Browse files
vaibhav-patelcopybara-github
authored andcommitted
feat: allow configuring Vertex AI API version
Merge #6183 Fixes #3246 PiperOrigin-RevId: 964386853
1 parent 4f07c93 commit ede87c2

2 files changed

Lines changed: 206 additions & 1 deletion

File tree

src/google/adk/models/google_llm.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import copy
2121
from functools import cached_property
2222
import logging
23+
import os
2324
import re
2425
from typing import Any
2526
from typing import AsyncGenerator
@@ -58,6 +59,7 @@
5859
_NEW_LINE = '\n'
5960
_EXCLUDED_PART_FIELD = {'inline_data': {'data'}}
6061
_GOOGLE_API_VERSION_SUFFIX_PATTERN = re.compile(r'/?(v[0-9][a-z0-9.-]*)/?')
62+
_API_VERSION_ENV_VARIABLE_NAME = 'GOOGLE_GENAI_API_VERSION'
6163

6264

6365
_RESOURCE_EXHAUSTED_POSSIBLE_FIX_MESSAGE = """
@@ -127,6 +129,27 @@ def api_client(self) -> Client:
127129
base_url: Optional[str] = None
128130
"""The base URL for the AI platform service endpoint."""
129131

132+
api_version: Optional[str] = None
133+
"""The API version to use for the AI platform service endpoint.
134+
135+
For the Vertex AI backend the google-genai SDK defaults to ``v1beta1``, which
136+
exposes the latest preview features. Production deployments that require a
137+
stable, SLA-eligible endpoint can set this to ``v1`` to use the GA Vertex AI
138+
API. When unset, the ``GOOGLE_GENAI_API_VERSION`` environment variable is
139+
consulted, and finally the SDK's own default is used so existing behavior is
140+
unchanged.
141+
142+
An API version embedded in the ``base_url`` path (e.g. a trailing ``/v1``)
143+
takes precedence over this field.
144+
145+
Sample:
146+
```python
147+
from google.adk.models import Gemini
148+
149+
agent = Agent(model=Gemini(model="gemini-2.5-pro", api_version="v1"))
150+
```
151+
"""
152+
130153
speech_config: Optional[types.SpeechConfig] = None
131154

132155
use_interactions_api: bool = False
@@ -237,7 +260,9 @@ async def generate_content_async(
237260
llm_request.config.http_options.headers or {}
238261
)
239262
_, api_version = self._base_url_and_api_version
240-
if api_version:
263+
if api_version is None:
264+
api_version = self.api_version
265+
if api_version and not llm_request.config.http_options.api_version:
241266
llm_request.config.http_options.api_version = api_version
242267

243268
try:
@@ -359,6 +384,8 @@ def api_client(self) -> Client:
359384
from google.genai import Client
360385

361386
base_url, api_version = self._base_url_and_api_version
387+
if api_version is None:
388+
api_version = self._configured_api_version()
362389
kwargs_for_http_options: dict[str, Any] = {
363390
'headers': self._tracking_headers(),
364391
'retry_options': self.retry_options,
@@ -390,6 +417,21 @@ def _api_backend(self) -> GoogleLLMVariant:
390417
def _tracking_headers(self) -> dict[str, str]:
391418
return get_tracking_headers()
392419

420+
def _configured_api_version(self) -> Optional[str]:
421+
"""Returns the explicitly configured API version, if any.
422+
423+
Resolution order:
424+
1. The ``api_version`` field set on this instance.
425+
2. The ``GOOGLE_GENAI_API_VERSION`` environment variable.
426+
427+
Returns ``None`` when neither is set, in which case the google-genai SDK's
428+
own default (``v1beta1`` for Vertex AI) applies, preserving existing
429+
behavior.
430+
"""
431+
if self.api_version:
432+
return self.api_version
433+
return os.environ.get(_API_VERSION_ENV_VARIABLE_NAME) or None
434+
393435
@cached_property
394436
def _base_url_and_api_version(self) -> tuple[Optional[str], Optional[str]]:
395437
return _normalize_base_url_and_api_version(self.base_url)

tests/unittests/models/test_google_llm.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,63 @@ def test_api_client_preserves_custom_base_url_path():
356356
assert client._api_client._http_options.api_version == "v1beta"
357357

358358

359+
def test_api_client_default_api_version_unchanged(monkeypatch):
360+
"""Without configuration, ADK does not force an api_version (SDK default)."""
361+
monkeypatch.delenv("GOOGLE_GENAI_API_VERSION", raising=False)
362+
model = Gemini(model="gemini-2.5-flash")
363+
364+
# ADK leaves api_version unset so the google-genai SDK applies its own
365+
# default (v1beta1 for Vertex AI), preserving existing behavior.
366+
assert model._base_url_and_api_version == (None, None)
367+
client = model.api_client
368+
assert client._api_client._http_options.api_version == "v1beta"
369+
370+
371+
def test_api_client_uses_api_version_field():
372+
"""The api_version field flows into the constructed client's http_options."""
373+
model = Gemini(model="gemini-2.5-flash", api_version="v1")
374+
375+
client = model.api_client
376+
377+
assert client._api_client._http_options.api_version == "v1"
378+
379+
380+
def test_api_client_uses_api_version_env_var(monkeypatch):
381+
"""The GOOGLE_GENAI_API_VERSION env var flows into http_options."""
382+
monkeypatch.setenv("GOOGLE_GENAI_API_VERSION", "v1")
383+
model = Gemini(model="gemini-2.5-flash")
384+
385+
client = model.api_client
386+
387+
assert client._api_client._http_options.api_version == "v1"
388+
389+
390+
def test_api_version_field_overrides_env_var(monkeypatch):
391+
"""The explicit api_version field takes precedence over the env var."""
392+
monkeypatch.setenv("GOOGLE_GENAI_API_VERSION", "v1beta1")
393+
model = Gemini(model="gemini-2.5-flash", api_version="v1")
394+
395+
client = model.api_client
396+
397+
assert client._api_client._http_options.api_version == "v1"
398+
399+
400+
def test_base_url_api_version_overrides_field():
401+
"""A version embedded in base_url wins over the api_version field."""
402+
model = Gemini(
403+
model="gemini-2.5-flash",
404+
base_url="https://generativelanguage.googleapis.com/v1alpha",
405+
api_version="v1",
406+
)
407+
408+
client = model.api_client
409+
410+
assert client._api_client._http_options.base_url == (
411+
"https://generativelanguage.googleapis.com/"
412+
)
413+
assert client._api_client._http_options.api_version == "v1alpha"
414+
415+
359416
def test_maybe_append_user_content(gemini_llm, llm_request):
360417
# Test with user content already present
361418
gemini_llm._maybe_append_user_content(llm_request)
@@ -788,6 +845,90 @@ async def mock_coro():
788845
assert len(responses) == 2 if stream else 1
789846

790847

848+
@pytest.mark.asyncio
849+
async def test_generate_content_async_patches_api_version_from_field(
850+
llm_request, generate_content_response
851+
):
852+
"""The configured api_version field is patched onto the request config."""
853+
gemini_llm = Gemini(model="gemini-2.5-flash", api_version="v1")
854+
llm_request.config.http_options = types.HttpOptions(
855+
headers={"custom-header": "custom-value"}
856+
)
857+
858+
with mock.patch.object(gemini_llm, "api_client") as mock_client:
859+
860+
async def mock_coro():
861+
return generate_content_response
862+
863+
mock_client.aio.models.generate_content.return_value = mock_coro()
864+
865+
_ = [
866+
resp
867+
async for resp in gemini_llm.generate_content_async(
868+
llm_request, stream=False
869+
)
870+
]
871+
872+
call_args = mock_client.aio.models.generate_content.call_args
873+
final_config = call_args.kwargs["config"]
874+
assert final_config.http_options.api_version == "v1"
875+
876+
877+
@pytest.mark.asyncio
878+
async def test_generate_content_async_does_not_override_request_api_version(
879+
llm_request, generate_content_response
880+
):
881+
"""Request-level api_version takes precedence over model-level configuration."""
882+
gemini_llm = Gemini(model="gemini-2.5-flash", api_version="v1")
883+
llm_request.config.http_options = types.HttpOptions(api_version="v2")
884+
885+
with mock.patch.object(gemini_llm, "api_client") as mock_client:
886+
887+
async def mock_coro():
888+
return generate_content_response
889+
890+
mock_client.aio.models.generate_content.return_value = mock_coro()
891+
892+
_ = [
893+
resp
894+
async for resp in gemini_llm.generate_content_async(
895+
llm_request, stream=False
896+
)
897+
]
898+
899+
call_args = mock_client.aio.models.generate_content.call_args
900+
final_config = call_args.kwargs["config"]
901+
assert final_config.http_options.api_version == "v2"
902+
903+
904+
@pytest.mark.asyncio
905+
async def test_generate_content_async_env_var_does_not_override_custom_client_api_version(
906+
llm_request, generate_content_response, monkeypatch
907+
):
908+
"""The GOOGLE_GENAI_API_VERSION env var does not override client-level custom configuration."""
909+
monkeypatch.setenv("GOOGLE_GENAI_API_VERSION", "env-version")
910+
gemini_llm = Gemini(model="gemini-2.5-flash")
911+
llm_request.config.http_options = types.HttpOptions()
912+
913+
with mock.patch.object(gemini_llm, "api_client") as mock_client:
914+
915+
async def mock_coro():
916+
return generate_content_response
917+
918+
mock_client.aio.models.generate_content.return_value = mock_coro()
919+
920+
_ = [
921+
resp
922+
async for resp in gemini_llm.generate_content_async(
923+
llm_request, stream=False
924+
)
925+
]
926+
927+
call_args = mock_client.aio.models.generate_content.call_args
928+
final_config = call_args.kwargs["config"]
929+
assert final_config.http_options.api_version is None
930+
931+
791932
def test_live_api_version_vertex_ai(gemini_llm):
792933
"""Test that _live_api_version returns 'v1beta1' for Vertex AI backend."""
793934
with mock.patch.object(
@@ -796,6 +937,28 @@ def test_live_api_version_vertex_ai(gemini_llm):
796937
assert gemini_llm._live_api_version == "v1beta1"
797938

798939

940+
def test_live_api_version_ignores_configured_field():
941+
"""Test that _live_api_version ignores the configured api_version field."""
942+
gemini_llm = Gemini(model="gemini-2.5-flash", api_version="v1")
943+
944+
with mock.patch.object(
945+
gemini_llm, "_api_backend", GoogleLLMVariant.VERTEX_AI
946+
):
947+
assert gemini_llm._live_api_version == "v1beta1"
948+
949+
950+
def test_live_api_client_ignores_configured_field():
951+
"""Test that _live_api_client http_options ignores the api_version field."""
952+
gemini_llm = Gemini(model="gemini-2.5-flash", api_version="v1")
953+
954+
with mock.patch.object(
955+
gemini_llm, "_api_backend", GoogleLLMVariant.VERTEX_AI
956+
):
957+
client = gemini_llm._live_api_client
958+
959+
assert client._api_client._http_options.api_version == "v1beta1"
960+
961+
799962
def test_live_api_version_uses_google_base_url_version():
800963
gemini_llm = Gemini(
801964
model="gemini-2.5-flash",

0 commit comments

Comments
 (0)