Skip to content

Commit 98747f8

Browse files
authored
Fix Vertex AI hook silently discarding credentials when vertexai flag is set (#72012)
1 parent b03e48a commit 98747f8

5 files changed

Lines changed: 116 additions & 27 deletions

File tree

‎providers/common/ai/docs/connections/pydantic_ai_vertex.rst‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,14 @@ Force Vertex AI Mode
6060
above already makes ``GoogleCloudProvider`` hard-code ``vertexai=True``
6161
unconditionally when it builds its client.
6262

63-
.. important::
64-
Leave this field unset. Setting it currently breaks the connection:
65-
neither ``GoogleProvider`` nor ``GoogleCloudProvider`` accept a
66-
``vertexai`` constructor argument, so the hook silently discards
67-
every other field on this connection (project, location, service
68-
account, API key) and falls back to resolving credentials from
69-
environment variables only. If auth unexpectedly falls back to env
70-
vars, check the task log for a "rejected kwargs" warning.
63+
.. note::
64+
This field is accepted for backward compatibility but has no effect:
65+
it is never forwarded to the provider, and every other field on the
66+
connection (project, location, service account, API key) is passed
67+
through normally. Setting it logs a warning in the task log noting
68+
that the field is ignored and that Vertex AI vs. Generative Language
69+
API mode is selected via the model prefix (``google-cloud:`` vs.
70+
``google:``) instead.
7171

7272
API Key
7373
Google API key for Vertex AI Express Mode. Falls back to the

‎providers/common/ai/provider.yaml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ connection-types:
310310
conn-fields:
311311
model:
312312
label: Model
313-
description: "Google model identifier (e.g. google-vertex:gemini-2.0-flash)"
313+
description: "Google model identifier (e.g. google-cloud:gemini-2.0-flash)"
314314
schema:
315315
type:
316316
- string
@@ -331,7 +331,7 @@ connection-types:
331331
- 'null'
332332
vertexai:
333333
label: Force Vertex AI Mode
334-
description: "Force Vertex AI mode. Auto-detected when project/location/credentials are set."
334+
description: "Ignored (kept for compatibility); mode is now selected via the Model field's prefix."
335335
schema:
336336
type:
337337
- boolean

‎providers/common/ai/src/airflow/providers/common/ai/get_provider_info.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def get_provider_info():
248248
"conn-fields": {
249249
"model": {
250250
"label": "Model",
251-
"description": "Google model identifier (e.g. google-vertex:gemini-2.0-flash)",
251+
"description": "Google model identifier (e.g. google-cloud:gemini-2.0-flash)",
252252
"schema": {"type": ["string", "null"]},
253253
},
254254
"project": {
@@ -263,7 +263,7 @@ def get_provider_info():
263263
},
264264
"vertexai": {
265265
"label": "Force Vertex AI Mode",
266-
"description": "Force Vertex AI mode. Auto-detected when project/location/credentials are set.",
266+
"description": "Ignored (kept for compatibility); mode is now selected via the Model field's prefix.",
267267
"schema": {"type": ["boolean", "null"]},
268268
},
269269
"api_key": {

‎providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -459,21 +459,22 @@ class PydanticAIVertexHook(PydanticAIHook):
459459
- **extra** JSON::
460460
461461
{
462-
"model": "google-vertex:gemini-2.0-flash",
462+
"model": "google-cloud:gemini-2.0-flash",
463463
"project": "my-gcp-project",
464464
"location": "us-central1",
465465
"service_account_info": {...},
466-
"vertexai": true,
467466
}
468467
469468
Use ``"service_account_info"`` to embed the service-account JSON directly
470469
(as an object, not a string path).
471470
472-
Set ``"vertexai": true`` to force Vertex AI mode when only ``api_key`` is
473-
provided. Omit ``vertexai`` for the Generative Language API (GLA).
471+
``"vertexai"`` is accepted for backward compatibility but has no effect:
472+
pydantic-ai now selects Vertex AI vs. the Generative Language API from the
473+
model prefix (``google-cloud:`` vs. ``google:``) rather than a
474+
constructor flag, so there is nothing left for this field to control.
474475
475476
:param llm_conn_id: Airflow connection ID.
476-
:param model_id: Model identifier, e.g. ``"google-vertex:gemini-2.0-flash"``.
477+
:param model_id: Model identifier, e.g. ``"google-cloud:gemini-2.0-flash"``.
477478
"""
478479

479480
conn_type = "pydanticai-vertex"
@@ -488,8 +489,8 @@ def get_ui_field_behaviour() -> dict[str, Any]:
488489
"relabeling": {},
489490
"placeholders": {
490491
"extra": (
491-
'{"model": "google-vertex:gemini-2.0-flash", '
492-
'"project": "my-project", "location": "us-central1", "vertexai": true}'
492+
'{"model": "google-cloud:gemini-2.0-flash", '
493+
'"project": "my-project", "location": "us-central1"}'
493494
" — add service_account_info (object) for SA auth;"
494495
" omit both to use Application Default Credentials"
495496
),
@@ -510,10 +511,18 @@ def _get_provider_kwargs(
510511
if extra.get(_key):
511512
kwargs[_key] = extra[_key]
512513

513-
# Optional vertexai bool flag (force Vertex AI mode for API-key auth).
514-
_vertexai = extra.get("vertexai")
515-
if _vertexai is not None:
516-
kwargs["vertexai"] = bool(_vertexai)
514+
# "vertexai" predates pydantic-ai splitting GoogleProvider (Generative Language API)
515+
# from GoogleCloudProvider (Vertex AI, which hardcodes vertexai=True internally and
516+
# accepts no such constructor kwarg) in pydantic/pydantic-ai#5336. Forwarding it would
517+
# raise TypeError, which the base hook's `except TypeError` in get_conn() would then
518+
# swallow by falling back to env-var auth with *all* other kwargs discarded — silently
519+
# authenticating as the wrong identity. Accept the field for backward compatibility but
520+
# never forward it: which API is used is now controlled by the model prefix.
521+
if extra.get("vertexai") is not None:
522+
self.log.warning(
523+
"The 'vertexai' connection field is ignored; Vertex AI vs. Generative Language "
524+
"API mode is now selected via the model prefix ('google-cloud:' vs. 'google:')."
525+
)
517526

518527
# Service-account credentials — loaded lazily to avoid importing
519528
# google-auth on non-Vertex code paths (optional heavy dependency).

‎providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py‎

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -743,15 +743,31 @@ def test_get_provider_kwargs_api_key_gla_mode(self):
743743
)
744744
assert result["api_key"] == "gla-key"
745745

746-
def test_get_provider_kwargs_vertexai_flag(self):
747-
"""vertexai bool is forwarded and coerced to bool."""
746+
@pytest.mark.parametrize("vertexai_value", [True, False])
747+
def test_get_provider_kwargs_vertexai_flag_is_not_forwarded(self, vertexai_value):
748+
"""The ``vertexai`` extra field must never reach the provider constructor.
749+
750+
Neither ``GoogleProvider`` nor ``GoogleCloudProvider`` in current pydantic-ai
751+
accept a ``vertexai`` kwarg (pydantic/pydantic-ai#5336 hardcoded it inside
752+
``GoogleCloudProvider`` instead). Forwarding it raises ``TypeError``, which the
753+
base hook's fallback then swallows by dropping every other kwarg -- silently
754+
re-resolving credentials from the environment. Regression test for that bug.
755+
"""
748756
hook = PydanticAIVertexHook.__new__(PydanticAIVertexHook)
749757
result = hook._get_provider_kwargs(
750758
None,
751759
None,
752-
{"model": "google-vertex:gemini-2.0-flash", "api_key": "key", "vertexai": True},
760+
{
761+
"model": "google-vertex:gemini-2.0-flash",
762+
"project": "my-project",
763+
"location": "us-central1",
764+
"vertexai": vertexai_value,
765+
},
753766
)
754-
assert result["vertexai"] is True
767+
assert "vertexai" not in result
768+
# The other credential kwargs must still go through untouched.
769+
assert result["project"] == "my-project"
770+
assert result["location"] == "us-central1"
755771

756772
def test_get_provider_kwargs_service_account_info_loads_credentials(self):
757773
"""service_account_info dict is loaded into a Credentials object."""
@@ -833,3 +849,67 @@ def test_get_conn_uses_explicit_project(self, mock_infer_provider_class, mock_in
833849
factory = mock_infer_model.call_args[1]["provider_factory"]
834850
factory("google-vertex")
835851
mock_provider_cls.assert_called_with(project="my-project", location="europe-west4")
852+
853+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_model", autospec=True)
854+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_provider", autospec=True)
855+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_provider_class", autospec=True)
856+
def test_get_conn_vertexai_flag_does_not_trigger_typeerror_fallback(
857+
self, mock_infer_provider_class, mock_infer_provider, mock_infer_model
858+
):
859+
"""Setting ``vertexai`` must not push ``get_conn`` onto the ``except TypeError``
860+
fallback path, which would silently discard project/location/credentials.
861+
862+
The stand-in below has the exact keyword-only signature of the real
863+
``GoogleCloudProvider.__init__`` (verified against the installed pydantic-ai) so
864+
it raises ``TypeError`` on an unexpected ``vertexai`` kwarg exactly like the real
865+
class would -- the real class itself needs the optional ``google-genai``
866+
dependency, which isn't part of this provider's test environment.
867+
"""
868+
869+
class FakeGoogleCloudProvider:
870+
def __init__(
871+
self,
872+
*,
873+
api_key=None,
874+
credentials=None,
875+
project=None,
876+
location=None,
877+
client=None,
878+
http_client=None,
879+
base_url=None,
880+
retry_options=None,
881+
):
882+
self.kwargs = {
883+
"api_key": api_key,
884+
"credentials": credentials,
885+
"project": project,
886+
"location": location,
887+
}
888+
889+
mock_infer_model.return_value = MagicMock(spec=Model)
890+
mock_infer_provider_class.return_value = FakeGoogleCloudProvider
891+
892+
hook = PydanticAIVertexHook(llm_conn_id="vertex_test")
893+
conn = Connection(
894+
conn_id="vertex_test",
895+
conn_type="pydanticai-vertex",
896+
extra=json.dumps(
897+
{
898+
"model": "google-cloud:gemini-2.0-flash",
899+
"project": "my-project",
900+
"location": "us-central1",
901+
"vertexai": True,
902+
}
903+
),
904+
)
905+
with patch.object(hook, "get_connection", return_value=conn):
906+
hook.get_conn()
907+
908+
factory = mock_infer_model.call_args[1]["provider_factory"]
909+
provider = factory("google-cloud")
910+
911+
assert isinstance(provider, FakeGoogleCloudProvider)
912+
assert provider.kwargs["project"] == "my-project"
913+
assert provider.kwargs["location"] == "us-central1"
914+
# The TypeError fallback must never have been reached.
915+
mock_infer_provider.assert_not_called()

0 commit comments

Comments
 (0)