Skip to content

Commit 7f0e22b

Browse files
committed
refactor(apigee): replace scope-based auth with credentials injection
Per reviewer feedback, drop the internal google.auth.default() call and the _APIGEE_SCOPES constant. Instead, expose an opt-in credentials parameter on ApigeeLlm.__init__ that is forwarded to genai.Client when provided. When omitted, the credentials kwarg is not forwarded at all, preserving the default genai.Client auth flow (and avoiding its Gemini Developer API warning about credentials usage). Callers needing additional OAuth scopes (e.g., userinfo.email for Apigee tokeninfo identification, the original #4721 use case) can now construct credentials with their preferred scopes and inject them.
1 parent 1d6e9b0 commit 7f0e22b

2 files changed

Lines changed: 45 additions & 26 deletions

File tree

src/google/adk/models/apigee_llm.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from typing import TYPE_CHECKING
3131

3232
from google.adk import version as adk_version
33-
import google.auth
3433
from google.genai import types
3534
import httpx
3635
import tenacity
@@ -41,6 +40,7 @@
4140
from .llm_response import LlmResponse
4241

4342
if TYPE_CHECKING:
43+
from google.auth.credentials import Credentials
4444
from google.genai import Client
4545

4646
from .llm_request import LlmRequest
@@ -53,11 +53,6 @@
5353
_PROJECT_ENV_VARIABLE_NAME = 'GOOGLE_CLOUD_PROJECT'
5454
_LOCATION_ENV_VARIABLE_NAME = 'GOOGLE_CLOUD_LOCATION'
5555

56-
_APIGEE_SCOPES = [
57-
'https://www.googleapis.com/auth/cloud-platform',
58-
'https://www.googleapis.com/auth/userinfo.email',
59-
]
60-
6156
_CUSTOM_METADATA_FIELDS = (
6257
'id',
6358
'created',
@@ -98,6 +93,7 @@ def __init__(
9893
custom_headers: dict[str, str] | None = None,
9994
retry_options: Optional[types.HttpRetryOptions] = None,
10095
api_type: ApiType | str = ApiType.UNKNOWN,
96+
credentials: Optional[Credentials] = None,
10197
):
10298
"""Initializes the Apigee LLM backend.
10399
@@ -129,6 +125,11 @@ def __init__(
129125
authorization headers in Vertex AI and Gemini API calls.
130126
retry_options: Allow google-genai to retry failed responses.
131127
api_type: The type of API to use. One of `ApiType` or string.
128+
credentials: Optional google-auth credentials passed through to the
129+
underlying `genai.Client`. Use this when the Apigee proxy requires
130+
additional OAuth scopes (e.g., `userinfo.email` for tokeninfo-based
131+
caller identification). When omitted, the default `genai.Client`
132+
authentication flow is used.
132133
""" # fmt: skip
133134

134135
super().__init__(model=model, retry_options=retry_options)
@@ -171,6 +172,7 @@ def __init__(
171172
)
172173
self._custom_headers = custom_headers or {}
173174
self._user_agent = f'google-adk/{adk_version.__version__}'
175+
self._credentials = credentials
174176

175177
@classmethod
176178
@override
@@ -240,16 +242,15 @@ def api_client(self) -> Client:
240242
**kwargs_for_http_options,
241243
)
242244

243-
credentials, _ = google.auth.default(scopes=_APIGEE_SCOPES)
244-
245245
kwargs_for_client = {}
246246
kwargs_for_client['vertexai'] = self._isvertexai
247247
if self._isvertexai:
248248
kwargs_for_client['project'] = self._project
249249
kwargs_for_client['location'] = self._location
250+
if self._credentials is not None:
251+
kwargs_for_client['credentials'] = self._credentials
250252

251253
return Client(
252-
credentials=credentials,
253254
http_options=http_options,
254255
**kwargs_for_client,
255256
)

tests/unittests/models/test_apigee_llm.py

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from unittest import mock
1919
from unittest.mock import AsyncMock
2020

21-
from google.adk.models.apigee_llm import _APIGEE_SCOPES
2221
from google.adk.models.apigee_llm import ApigeeLlm
2322
from google.adk.models.apigee_llm import CompletionsHTTPClient
2423
from google.adk.models.llm_request import LlmRequest
@@ -34,16 +33,6 @@
3433
PROXY_URL = 'https://test.apigee.net'
3534

3635

37-
@pytest.fixture(autouse=True)
38-
def mock_google_auth_default():
39-
"""Mocks google.auth.default to avoid requiring real credentials in tests."""
40-
with mock.patch(
41-
'google.adk.models.apigee_llm.google.auth.default'
42-
) as mock_auth:
43-
mock_auth.return_value = (mock.Mock(), 'test-project')
44-
yield mock_auth
45-
46-
4736
@pytest.fixture
4837
def llm_request():
4938
"""Provides a sample LlmRequest for testing."""
@@ -664,12 +653,11 @@ def test_parse_response_usage_metadata():
664653

665654
@pytest.mark.asyncio
666655
@mock.patch('google.genai.Client')
667-
async def test_api_client_requests_userinfo_email_scope(
668-
mock_client_constructor, llm_request, mock_google_auth_default
656+
async def test_api_client_passes_credentials_when_provided(
657+
mock_client_constructor, llm_request
669658
):
670-
"""Tests that api_client requests userinfo.email scope for Apigee Gateway tokeninfo."""
659+
"""Tests that credentials passed to __init__ are forwarded to genai.Client."""
671660
mock_credentials = mock.Mock()
672-
mock_google_auth_default.return_value = (mock_credentials, 'test-project')
673661

674662
mock_client_instance = mock.Mock()
675663
mock_client_instance.aio.models.generate_content = AsyncMock(
@@ -689,15 +677,45 @@ async def test_api_client_requests_userinfo_email_scope(
689677
apigee_llm = ApigeeLlm(
690678
model=APIGEE_GEMINI_MODEL_ID,
691679
proxy_url=PROXY_URL,
680+
credentials=mock_credentials,
692681
)
693682
_ = [resp async for resp in apigee_llm.generate_content_async(llm_request)]
694683

695-
mock_google_auth_default.assert_called_once_with(scopes=_APIGEE_SCOPES)
696-
697684
_, kwargs = mock_client_constructor.call_args
698685
assert kwargs['credentials'] is mock_credentials
699686

700687

688+
@pytest.mark.asyncio
689+
@mock.patch('google.genai.Client')
690+
async def test_api_client_omits_credentials_when_not_provided(
691+
mock_client_constructor, llm_request
692+
):
693+
"""Tests that credentials kwarg is not forwarded when not supplied."""
694+
mock_client_instance = mock.Mock()
695+
mock_client_instance.aio.models.generate_content = AsyncMock(
696+
return_value=types.GenerateContentResponse(
697+
candidates=[
698+
types.Candidate(
699+
content=Content(
700+
parts=[Part.from_text(text='Test response')],
701+
role='model',
702+
)
703+
)
704+
]
705+
)
706+
)
707+
mock_client_constructor.return_value = mock_client_instance
708+
709+
apigee_llm = ApigeeLlm(
710+
model=APIGEE_GEMINI_MODEL_ID,
711+
proxy_url=PROXY_URL,
712+
)
713+
_ = [resp async for resp in apigee_llm.generate_content_async(llm_request)]
714+
715+
_, kwargs = mock_client_constructor.call_args
716+
assert 'credentials' not in kwargs
717+
718+
701719
def test_parse_response_with_refusal():
702720
"""Tests that CompletionsHTTPClient parses refusal correctly."""
703721
client = CompletionsHTTPClient(base_url='http://test')

0 commit comments

Comments
 (0)