Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions agentplatform/_genai/model_garden.py
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,33 @@ def get_batch_prediction_job(
_DEFAULT_DEPLOY_TIMEOUT_SECONDS = 2 * 60 * 60
_DEFAULT_DEPLOY_POLL_INTERVAL_SECONDS = 30

# Model Garden catalog reads (ListPublisherModels, GetPublisherModel) are
# served from us-central1, as in vertexai.model_garden. Other regional
# endpoints return a partial catalog (e.g. no deploy configs for models that
# deploy fine there, since Deploy reads the global model configs).
_CATALOG_BASE_URL = "https://us-central1-aiplatform.googleapis.com/"

@staticmethod
def _catalog_http_options(
api_client: Any,
) -> Optional[genai_types.HttpOptions]:
"""Returns ``http_options`` that send catalog reads to us-central1.

Only the catalog reads are routed this way; deploy, export and batch
requests still go to the client's location.

Args:
api_client: The client's API client.

Returns:
``HttpOptions`` with the us-central1 base URL, or None when the client
was created with a custom ``base_url`` (e.g. a proxy or private
endpoint), which is then left unchanged.
"""
if api_client.custom_base_url:
return None
return genai_types.HttpOptions(base_url=ModelGarden._CATALOG_BASE_URL)

@staticmethod
def _build_filter_str(
model_filter: Optional[str],
Expand Down Expand Up @@ -1710,6 +1737,7 @@ def _list_all_publisher_models(
filter=api_config.filter,
list_all_versions=api_config.list_all_versions,
page_token=page_token,
http_options=api_config.http_options,
)
response = self._list_publisher_models(
parent="publishers/*",
Expand Down Expand Up @@ -1746,6 +1774,7 @@ def _list(
api_config = types.ListPublisherModelsConfig(
filter=filter_str,
list_all_versions=True,
http_options=ModelGarden._catalog_http_options(self._api_client),
)

models = self._list_all_publisher_models(api_config)
Expand Down Expand Up @@ -1859,6 +1888,7 @@ def list_publisher_model_deploy_options(
get_publisher_model_config = types.GetPublisherModelConfig(
is_hugging_face_model=self._is_hugging_face_model(model),
include_equivalent_model_garden_model_deployment_configs=True,
http_options=ModelGarden._catalog_http_options(self._api_client),
)
publisher_model = self._get_publisher_model(
name=self._reconcile_model_name(model),
Expand Down Expand Up @@ -3189,6 +3219,7 @@ async def _list_all_publisher_models(
filter=api_config.filter,
list_all_versions=api_config.list_all_versions,
page_token=page_token,
http_options=api_config.http_options,
)
response = await self._list_publisher_models(
parent="publishers/*",
Expand Down Expand Up @@ -3225,6 +3256,7 @@ async def _list(
api_config = types.ListPublisherModelsConfig(
filter=filter_str,
list_all_versions=True,
http_options=ModelGarden._catalog_http_options(self._api_client),
)

models = await self._list_all_publisher_models(api_config)
Expand Down Expand Up @@ -3336,6 +3368,7 @@ async def list_publisher_model_deploy_options(
api_config = types.GetPublisherModelConfig(
is_hugging_face_model=ModelGarden._is_hugging_face_model(model),
include_equivalent_model_garden_model_deployment_configs=True,
http_options=ModelGarden._catalog_http_options(self._api_client),
)
publisher_model = await self._get_publisher_model(
name=ModelGarden._reconcile_model_name(model), config=api_config
Expand Down
123 changes: 123 additions & 0 deletions tests/unit/agentplatform/genai/test_genai_model_garden.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,18 @@
_OPEN_MODEL = "google/gemma3@gemma-3-12b-it"


_CATALOG_HTTP_OPTIONS = model_garden.genai_types.HttpOptions(
base_url="https://us-central1-aiplatform.googleapis.com/"
)


@pytest.fixture
def mock_client():
mock_api_client = mock.Mock(spec=client.Client)
mock_api_client.project = _TEST_PROJECT
mock_api_client.location = _TEST_LOCATION
mock_api_client.vertexai = True
mock_api_client.custom_base_url = None

return model_garden.ModelGarden(mock_api_client)

Expand All @@ -44,6 +50,7 @@ def mock_async_client():
mock_api_client.project = _TEST_PROJECT
mock_api_client.location = _TEST_LOCATION
mock_api_client.vertexai = True
mock_api_client.custom_base_url = None

return model_garden.AsyncModelGarden(mock_api_client)

Expand Down Expand Up @@ -488,6 +495,7 @@ def test_list_publisher_model_deploy_options_basic(mock_client):
config=types.GetPublisherModelConfig(
is_hugging_face_model=False,
include_equivalent_model_garden_model_deployment_configs=True,
http_options=_CATALOG_HTTP_OPTIONS,
),
)
assert len(options) == 1
Expand Down Expand Up @@ -826,10 +834,125 @@ def test_list_publisher_model_deploy_options_hugging_face_model(mock_client):
config=types.GetPublisherModelConfig(
is_hugging_face_model=True,
include_equivalent_model_garden_model_deployment_configs=True,
http_options=_CATALOG_HTTP_OPTIONS,
),
)


# --- Catalog reads are served from us-central1 ---------------------------


def test_list_models_catalog_routed_to_us_central1(mock_client):
"""A client outside us-central1 still lists the catalog from us-central1."""
mock_client._api_client.location = "europe-west4"
with mock.patch.object(
mock_client,
"_list_publisher_models",
return_value=types.ListPublisherModelsResponse(publisher_models=[]),
) as mock_list:
mock_client.list_deployable_models()
mock_client.list_models()

for call in mock_list.call_args_list:
assert call.kwargs["config"].http_options == _CATALOG_HTTP_OPTIONS


def test_list_models_catalog_routing_kept_across_pages(mock_client):
"""Every page request carries the us-central1 base URL, not just the first."""
pages = [
types.ListPublisherModelsResponse(
publisher_models=[
_make_deployable_model("publishers/google/models/a")
],
next_page_token="page-2",
),
types.ListPublisherModelsResponse(
publisher_models=[
_make_deployable_model("publishers/google/models/b")
],
),
]
with mock.patch.object(
mock_client, "_list_publisher_models", side_effect=pages
) as mock_list:
models = mock_client.list_models()

assert models == ["google/a@001", "google/b@001"]
second_config = mock_list.call_args_list[1].kwargs["config"]
assert second_config.page_token == "page-2"
assert second_config.http_options == _CATALOG_HTTP_OPTIONS


def test_list_publisher_model_deploy_options_routed_to_us_central1(
mock_client,
):
"""GetPublisherModel is sent to us-central1 for a client in another region."""
mock_client._api_client.location = "asia-southeast1"
dummy_model = _make_model_with_deploy_options([_make_deploy_option()])
with mock.patch.object(
mock_client, "_get_publisher_model", return_value=dummy_model
) as mock_get:
mock_client.list_publisher_model_deploy_options(model=_OPEN_MODEL)

assert mock_get.call_args.kwargs["config"].http_options == (
_CATALOG_HTTP_OPTIONS
)


def test_catalog_reads_keep_custom_base_url(mock_client):
"""A client created with a custom base_url is not redirected."""
mock_client._api_client.custom_base_url = "https://my-proxy.example.com/"
dummy_model = _make_model_with_deploy_options([_make_deploy_option()])
with (
mock.patch.object(
mock_client,
"_list_publisher_models",
return_value=types.ListPublisherModelsResponse(publisher_models=[]),
) as mock_list,
mock.patch.object(
mock_client, "_get_publisher_model", return_value=dummy_model
) as mock_get,
):
mock_client.list_models()
mock_client.list_publisher_model_deploy_options(model=_OPEN_MODEL)

assert mock_list.call_args.kwargs["config"].http_options is None
assert mock_get.call_args.kwargs["config"].http_options is None


def test_catalog_reads_routed_to_us_central1_async(mock_async_client):
"""The async surface routes catalog reads the same way."""
mock_async_client._api_client.location = "europe-west4"
dummy_model = _make_model_with_deploy_options([_make_deploy_option()])
with (
mock.patch.object(
mock_async_client,
"_list_publisher_models",
new=mock.AsyncMock(
return_value=types.ListPublisherModelsResponse(
publisher_models=[]
)
),
) as mock_list,
mock.patch.object(
mock_async_client,
"_get_publisher_model",
new=mock.AsyncMock(return_value=dummy_model),
) as mock_get,
):
asyncio.run(mock_async_client.list_deployable_models())
asyncio.run(
mock_async_client.list_publisher_model_deploy_options(model=_OPEN_MODEL)
)

assert mock_list.call_args.kwargs["config"].http_options == (
_CATALOG_HTTP_OPTIONS
)
assert mock_get.call_args.kwargs["config"].http_options == (
_CATALOG_HTTP_OPTIONS
)


def test_list_publisher_model_deploy_options_async(mock_async_client):
"""Tests the async client returns deploy options."""
dummy_model = _make_model_with_deploy_options([_make_deploy_option()])
Expand Down
Loading