Skip to content
Merged
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
40 changes: 32 additions & 8 deletions google/genai/_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,17 +230,31 @@ def join_url_path(base_url: str, path: str) -> str:
return urlunparse(parsed_base._replace(path=base_path + '/' + path))


_MULTI_REGIONAL_REP_SUFFIXES = tuple(
f'.{loc}.rep.{domain}'
for loc in _MULTI_REGIONAL_LOCATIONS
for domain in ('sandbox.googleapis.com', 'googleapis.com')
)


def to_mtls_url(url: str) -> str:
"""Rewrites a googleapis.com URL to its mTLS endpoint.

For example, `aiplatform.googleapis.com` becomes
`aiplatform.mtls.googleapis.com` and `foo.sandbox.googleapis.com` becomes
`foo.mtls.sandbox.googleapis.com`. URLs on other hosts, and URLs that already
point at an mTLS endpoint, are returned unchanged.
`foo.mtls.sandbox.googleapis.com`. URLs on other hosts, URLs that already
point at an mTLS endpoint, and multi-regional `.rep.googleapis.com` endpoints
(which do not publish `.rep.mtls.googleapis.com` DNS records) are returned
unchanged.
"""
parsed = urlparse(url)
netloc = parsed.netloc
if netloc.endswith(('.mtls.googleapis.com', '.mtls.sandbox.googleapis.com')):
hostname = parsed.hostname or ''
if hostname.endswith(
('.mtls.googleapis.com', '.mtls.sandbox.googleapis.com')
):
return url
if hostname.endswith(_MULTI_REGIONAL_REP_SUFFIXES):
return url
for domain in ('sandbox.googleapis.com', 'googleapis.com'):
if netloc.endswith('.' + domain):
Expand Down Expand Up @@ -1600,7 +1614,9 @@ def _request_once(
self._authorized_session.configure_mtls_channel(
client_cert_source
) # type: ignore[no-untyped-call]
if self._authorized_session._is_mtls:
if self._use_mtls_endpoint(
client_cert_loaded=bool(self._authorized_session._is_mtls)
):
url = to_mtls_url(url)
response = self._authorized_session.request( # type: ignore[no-untyped-call]
method=http_request.method.upper(),
Expand Down Expand Up @@ -1677,7 +1693,9 @@ async def _async_request_once(
await session.configure_mtls_channel( # type: ignore[union-attr]
client_cert_source
)
if session._is_mtls: # type: ignore[union-attr]
if self._use_mtls_endpoint(
client_cert_loaded=bool(session._is_mtls) # type: ignore[union-attr]
):
url = to_mtls_url(url)
elif self._aiohttp_use_mtls_endpoint:
url = to_mtls_url(url)
Expand Down Expand Up @@ -1749,7 +1767,9 @@ async def _async_request_once(
await session.configure_mtls_channel( # type: ignore[union-attr]
client_cert_source
)
if session._is_mtls: # type: ignore[union-attr]
if self._use_mtls_endpoint(
client_cert_loaded=bool(session._is_mtls) # type: ignore[union-attr]
):
url = to_mtls_url(url)
elif self._aiohttp_use_mtls_endpoint:
url = to_mtls_url(url)
Expand Down Expand Up @@ -2162,7 +2182,9 @@ def _write_chunks(chunks: Iterator[bytes]) -> None:
self._authorized_session.configure_mtls_channel(
client_cert_source
) # type: ignore[no-untyped-call]
if self._authorized_session._is_mtls:
if self._use_mtls_endpoint(
client_cert_loaded=bool(self._authorized_session._is_mtls)
):
url = to_mtls_url(url)
if destination is not None:
response = self._authorized_session.request( # type: ignore[no-untyped-call]
Expand Down Expand Up @@ -2531,7 +2553,9 @@ async def _write_chunks(chunks: AsyncIterator[bytes]) -> None:
await session.configure_mtls_channel( # type: ignore[union-attr]
client_cert_source
)
if session._is_mtls: # type: ignore[union-attr]
if self._use_mtls_endpoint(
client_cert_loaded=bool(session._is_mtls) # type: ignore[union-attr]
):
url = to_mtls_url(url)
elif self._aiohttp_use_mtls_endpoint:
url = to_mtls_url(url)
Expand Down
92 changes: 92 additions & 0 deletions google/genai/tests/client/test_client_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,26 @@ async def run():
"https://us-central1-aiplatform.googleapis.com/v1/models",
"https://us-central1-aiplatform.mtls.googleapis.com/v1/models",
),
(
"https://secretmanager.us-central1.rep.googleapis.com/v1/secrets",
"https://secretmanager.us-central1.rep.mtls.googleapis.com/v1/secrets",
),
(
"https://aiplatform.us.rep.googleapis.com/v1/models",
"https://aiplatform.us.rep.googleapis.com/v1/models",
),
(
"https://aiplatform.eu.rep.googleapis.com/v1/models",
"https://aiplatform.eu.rep.googleapis.com/v1/models",
),
(
"https://aiplatform.us.rep.sandbox.googleapis.com/v1/models",
"https://aiplatform.us.rep.sandbox.googleapis.com/v1/models",
),
(
"https://aiplatform.eu.rep.sandbox.googleapis.com/v1/models",
"https://aiplatform.eu.rep.sandbox.googleapis.com/v1/models",
),
(
"wss://aiplatform.googleapis.com/ws",
"wss://aiplatform.mtls.googleapis.com/ws",
Expand Down Expand Up @@ -2392,3 +2412,75 @@ def test_google_auth_async_used_by_default(
api_client.has_aiohttp = True
client = _mtls_test_client()
assert client._api_client._use_google_auth_async()


@pytest.mark.parametrize("call", ["request", "download"])
def test_google_auth_sync_respects_mtls_endpoint_env_override(
mock_client_cert, monkeypatch, call
):
monkeypatch.setenv("GOOGLE_API_USE_MTLS_ENDPOINT", "never")
client = _mtls_test_client()
base_client = client._api_client
assert base_client._use_google_auth_sync()

mock_response = mock.MagicMock()
mock_response.status_code = 200
mock_response.headers = {}
mock_response.text = "{}"
mock_response.content = b""
mock_session = mock.MagicMock()
mock_session._is_mtls = True
mock_session.request = mock.Mock(return_value=mock_response)
base_client._authorized_session = mock_session

if call == "download":
base_client.download_file("files/abc:download")
else:
base_client.request("post", "models/gemini:generateContent", {})

assert mock_session.request.call_args.kwargs["url"].startswith(
"https://us-central1-aiplatform.googleapis.com/"
)


@requires_aiohttp
@pytest.mark.asyncio
@pytest.mark.parametrize("call", ["request", "stream", "download"])
async def test_google_auth_async_respects_mtls_endpoint_env_override(
mock_client_cert, mock_google_auth_aio, monkeypatch, call
):
from google.auth.transport import mtls

api_client.has_aiohttp = True
monkeypatch.setenv("GOOGLE_API_USE_MTLS_ENDPOINT", "never")
monkeypatch.setattr(mtls, "default_client_cert_source", lambda: None)
client = _mtls_test_client()
base_client = client._api_client
assert base_client._use_google_auth_async()

mock_response = mock.create_autospec(aiohttp.ClientResponse, instance=True)
mock_response.status = 200
mock_response.headers = {}
mock_response.text.return_value = "{}"
mock_response.read.return_value = b""
mock_session = mock.MagicMock()
mock_session._is_mtls = True
mock_session.configure_mtls_channel = mock.AsyncMock()
mock_session.request = mock.AsyncMock(return_value=mock_response)

with mock.patch.object(
base_client, "_get_aiohttp_session", return_value=mock_session
):
if call == "download":
await base_client.async_download_file("files/abc:download")
else:
http_request = base_client._build_request(
"post", "models/gemini:generateContent", {}, None
)
await base_client._async_request_once(
http_request, stream=call == "stream"
)

assert mock_session.request.call_args.kwargs["url"].startswith(
"https://us-central1-aiplatform.googleapis.com/"
)
Loading