This is an automated email from the ASF dual-hosted git repository.
Lee-W pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 98747f873f2 Fix Vertex AI hook silently discarding credentials when
vertexai flag is set (#72012)
98747f873f2 is described below
commit 98747f873f247f5bb378cb8012447103610a4fc3
Author: Wei Lee <[email protected]>
AuthorDate: Mon Aug 24 21:00:37 2026 +0900
Fix Vertex AI hook silently discarding credentials when vertexai flag is
set (#72012)
---
.../ai/docs/connections/pydantic_ai_vertex.rst | 16 ++--
providers/common/ai/provider.yaml | 4 +-
.../providers/common/ai/get_provider_info.py | 4 +-
.../providers/common/ai/hooks/pydantic_ai.py | 31 +++++---
.../tests/unit/common/ai/hooks/test_pydantic_ai.py | 88 +++++++++++++++++++++-
5 files changed, 116 insertions(+), 27 deletions(-)
diff --git a/providers/common/ai/docs/connections/pydantic_ai_vertex.rst
b/providers/common/ai/docs/connections/pydantic_ai_vertex.rst
index 8bebb7be313..a242b202de7 100644
--- a/providers/common/ai/docs/connections/pydantic_ai_vertex.rst
+++ b/providers/common/ai/docs/connections/pydantic_ai_vertex.rst
@@ -60,14 +60,14 @@ Force Vertex AI Mode
above already makes ``GoogleCloudProvider`` hard-code ``vertexai=True``
unconditionally when it builds its client.
- .. important::
- Leave this field unset. Setting it currently breaks the connection:
- neither ``GoogleProvider`` nor ``GoogleCloudProvider`` accept a
- ``vertexai`` constructor argument, so the hook silently discards
- every other field on this connection (project, location, service
- account, API key) and falls back to resolving credentials from
- environment variables only. If auth unexpectedly falls back to env
- vars, check the task log for a "rejected kwargs" warning.
+ .. note::
+ This field is accepted for backward compatibility but has no effect:
+ it is never forwarded to the provider, and every other field on the
+ connection (project, location, service account, API key) is passed
+ through normally. Setting it logs a warning in the task log noting
+ that the field is ignored and that Vertex AI vs. Generative Language
+ API mode is selected via the model prefix (``google-cloud:`` vs.
+ ``google:``) instead.
API Key
Google API key for Vertex AI Express Mode. Falls back to the
diff --git a/providers/common/ai/provider.yaml
b/providers/common/ai/provider.yaml
index 495946568e2..27d6dfac910 100644
--- a/providers/common/ai/provider.yaml
+++ b/providers/common/ai/provider.yaml
@@ -310,7 +310,7 @@ connection-types:
conn-fields:
model:
label: Model
- description: "Google model identifier (e.g.
google-vertex:gemini-2.0-flash)"
+ description: "Google model identifier (e.g.
google-cloud:gemini-2.0-flash)"
schema:
type:
- string
@@ -331,7 +331,7 @@ connection-types:
- 'null'
vertexai:
label: Force Vertex AI Mode
- description: "Force Vertex AI mode. Auto-detected when
project/location/credentials are set."
+ description: "Ignored (kept for compatibility); mode is now selected
via the Model field's prefix."
schema:
type:
- boolean
diff --git
a/providers/common/ai/src/airflow/providers/common/ai/get_provider_info.py
b/providers/common/ai/src/airflow/providers/common/ai/get_provider_info.py
index 3119f2ba45b..97ff10b5b41 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/get_provider_info.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/get_provider_info.py
@@ -248,7 +248,7 @@ def get_provider_info():
"conn-fields": {
"model": {
"label": "Model",
- "description": "Google model identifier (e.g.
google-vertex:gemini-2.0-flash)",
+ "description": "Google model identifier (e.g.
google-cloud:gemini-2.0-flash)",
"schema": {"type": ["string", "null"]},
},
"project": {
@@ -263,7 +263,7 @@ def get_provider_info():
},
"vertexai": {
"label": "Force Vertex AI Mode",
- "description": "Force Vertex AI mode. Auto-detected
when project/location/credentials are set.",
+ "description": "Ignored (kept for compatibility); mode
is now selected via the Model field's prefix.",
"schema": {"type": ["boolean", "null"]},
},
"api_key": {
diff --git
a/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
b/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
index c0bea0ddcbc..8b378818059 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
@@ -459,21 +459,22 @@ class PydanticAIVertexHook(PydanticAIHook):
- **extra** JSON::
{
- "model": "google-vertex:gemini-2.0-flash",
+ "model": "google-cloud:gemini-2.0-flash",
"project": "my-gcp-project",
"location": "us-central1",
"service_account_info": {...},
- "vertexai": true,
}
Use ``"service_account_info"`` to embed the service-account JSON
directly
(as an object, not a string path).
- Set ``"vertexai": true`` to force Vertex AI mode when only ``api_key``
is
- provided. Omit ``vertexai`` for the Generative Language API (GLA).
+ ``"vertexai"`` is accepted for backward compatibility but has no
effect:
+ pydantic-ai now selects Vertex AI vs. the Generative Language API from
the
+ model prefix (``google-cloud:`` vs. ``google:``) rather than a
+ constructor flag, so there is nothing left for this field to control.
:param llm_conn_id: Airflow connection ID.
- :param model_id: Model identifier, e.g.
``"google-vertex:gemini-2.0-flash"``.
+ :param model_id: Model identifier, e.g.
``"google-cloud:gemini-2.0-flash"``.
"""
conn_type = "pydanticai-vertex"
@@ -488,8 +489,8 @@ class PydanticAIVertexHook(PydanticAIHook):
"relabeling": {},
"placeholders": {
"extra": (
- '{"model": "google-vertex:gemini-2.0-flash", '
- '"project": "my-project", "location": "us-central1",
"vertexai": true}'
+ '{"model": "google-cloud:gemini-2.0-flash", '
+ '"project": "my-project", "location": "us-central1"}'
" — add service_account_info (object) for SA auth;"
" omit both to use Application Default Credentials"
),
@@ -510,10 +511,18 @@ class PydanticAIVertexHook(PydanticAIHook):
if extra.get(_key):
kwargs[_key] = extra[_key]
- # Optional vertexai bool flag (force Vertex AI mode for API-key auth).
- _vertexai = extra.get("vertexai")
- if _vertexai is not None:
- kwargs["vertexai"] = bool(_vertexai)
+ # "vertexai" predates pydantic-ai splitting GoogleProvider (Generative
Language API)
+ # from GoogleCloudProvider (Vertex AI, which hardcodes vertexai=True
internally and
+ # accepts no such constructor kwarg) in pydantic/pydantic-ai#5336.
Forwarding it would
+ # raise TypeError, which the base hook's `except TypeError` in
get_conn() would then
+ # swallow by falling back to env-var auth with *all* other kwargs
discarded — silently
+ # authenticating as the wrong identity. Accept the field for backward
compatibility but
+ # never forward it: which API is used is now controlled by the model
prefix.
+ if extra.get("vertexai") is not None:
+ self.log.warning(
+ "The 'vertexai' connection field is ignored; Vertex AI vs.
Generative Language "
+ "API mode is now selected via the model prefix
('google-cloud:' vs. 'google:')."
+ )
# Service-account credentials — loaded lazily to avoid importing
# google-auth on non-Vertex code paths (optional heavy dependency).
diff --git a/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
b/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
index 0290da3c133..c85e38046de 100644
--- a/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
+++ b/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
@@ -743,15 +743,31 @@ class TestPydanticAIVertexHook:
)
assert result["api_key"] == "gla-key"
- def test_get_provider_kwargs_vertexai_flag(self):
- """vertexai bool is forwarded and coerced to bool."""
+ @pytest.mark.parametrize("vertexai_value", [True, False])
+ def test_get_provider_kwargs_vertexai_flag_is_not_forwarded(self,
vertexai_value):
+ """The ``vertexai`` extra field must never reach the provider
constructor.
+
+ Neither ``GoogleProvider`` nor ``GoogleCloudProvider`` in current
pydantic-ai
+ accept a ``vertexai`` kwarg (pydantic/pydantic-ai#5336 hardcoded it
inside
+ ``GoogleCloudProvider`` instead). Forwarding it raises ``TypeError``,
which the
+ base hook's fallback then swallows by dropping every other kwarg --
silently
+ re-resolving credentials from the environment. Regression test for
that bug.
+ """
hook = PydanticAIVertexHook.__new__(PydanticAIVertexHook)
result = hook._get_provider_kwargs(
None,
None,
- {"model": "google-vertex:gemini-2.0-flash", "api_key": "key",
"vertexai": True},
+ {
+ "model": "google-vertex:gemini-2.0-flash",
+ "project": "my-project",
+ "location": "us-central1",
+ "vertexai": vertexai_value,
+ },
)
- assert result["vertexai"] is True
+ assert "vertexai" not in result
+ # The other credential kwargs must still go through untouched.
+ assert result["project"] == "my-project"
+ assert result["location"] == "us-central1"
def test_get_provider_kwargs_service_account_info_loads_credentials(self):
"""service_account_info dict is loaded into a Credentials object."""
@@ -833,3 +849,67 @@ class TestPydanticAIVertexHook:
factory = mock_infer_model.call_args[1]["provider_factory"]
factory("google-vertex")
mock_provider_cls.assert_called_with(project="my-project",
location="europe-west4")
+
+ @patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_model",
autospec=True)
+ @patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_provider",
autospec=True)
+
@patch("airflow.providers.common.ai.hooks.pydantic_ai.infer_provider_class",
autospec=True)
+ def test_get_conn_vertexai_flag_does_not_trigger_typeerror_fallback(
+ self, mock_infer_provider_class, mock_infer_provider, mock_infer_model
+ ):
+ """Setting ``vertexai`` must not push ``get_conn`` onto the ``except
TypeError``
+ fallback path, which would silently discard
project/location/credentials.
+
+ The stand-in below has the exact keyword-only signature of the real
+ ``GoogleCloudProvider.__init__`` (verified against the installed
pydantic-ai) so
+ it raises ``TypeError`` on an unexpected ``vertexai`` kwarg exactly
like the real
+ class would -- the real class itself needs the optional
``google-genai``
+ dependency, which isn't part of this provider's test environment.
+ """
+
+ class FakeGoogleCloudProvider:
+ def __init__(
+ self,
+ *,
+ api_key=None,
+ credentials=None,
+ project=None,
+ location=None,
+ client=None,
+ http_client=None,
+ base_url=None,
+ retry_options=None,
+ ):
+ self.kwargs = {
+ "api_key": api_key,
+ "credentials": credentials,
+ "project": project,
+ "location": location,
+ }
+
+ mock_infer_model.return_value = MagicMock(spec=Model)
+ mock_infer_provider_class.return_value = FakeGoogleCloudProvider
+
+ hook = PydanticAIVertexHook(llm_conn_id="vertex_test")
+ conn = Connection(
+ conn_id="vertex_test",
+ conn_type="pydanticai-vertex",
+ extra=json.dumps(
+ {
+ "model": "google-cloud:gemini-2.0-flash",
+ "project": "my-project",
+ "location": "us-central1",
+ "vertexai": True,
+ }
+ ),
+ )
+ with patch.object(hook, "get_connection", return_value=conn):
+ hook.get_conn()
+
+ factory = mock_infer_model.call_args[1]["provider_factory"]
+ provider = factory("google-cloud")
+
+ assert isinstance(provider, FakeGoogleCloudProvider)
+ assert provider.kwargs["project"] == "my-project"
+ assert provider.kwargs["location"] == "us-central1"
+ # The TypeError fallback must never have been reached.
+ mock_infer_provider.assert_not_called()