kaxil commented on code in PR #71437:
URL: https://github.com/apache/airflow/pull/71437#discussion_r4057057705


##########
providers/common/ai/docs/connections/pydantic_ai.rst:
##########
@@ -72,21 +97,31 @@ Host (optional)
 
 Extra (JSON, optional)
     A JSON object with additional configuration. Programmatic users can set the
-    model directly in extra:
+    LLM and embedding models directly in extra:
 
     .. code-block:: json
 
-        {"model": "openai:gpt-5.6-sol"}
+        {
+            "model": "openai:gpt-5.6-sol",
+            "embed_model": "openai:text-embedding-3-small"
+        }
 
-    When using the UI, the "Model" field above writes to this same location
-    automatically.
+    When using the UI, the "Model" and "Embedding Model" fields above write to
+    this same location automatically.
 
 Fallback Connections
     Other connection IDs to fail over to, in order, while this provider is
     unavailable. Stored in ``extra["fallback_conn_ids"]``. Entries may name any
     ``pydanticai`` connection type, so one chain can span vendors. See
     :doc:`/provider_fallback`.
 
+    Bedrock-specific fields include ``api_key``, ``base_url``, ``region_name``,

Review Comment:
   This paragraph describes `extra` fields but is indented under the Fallback 
Connections item that starts at :112, so it renders as part of the fallback 
description. The Extra (JSON, optional) item it belongs under ends at :110.



##########
providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py:
##########
@@ -220,8 +257,92 @@ def _seed_connection(self, conn: Connection) -> None:
         would fetch that same connection a second time the first time it runs, 
doubling the
         Execution API round trips a fallback chain costs.
         """
-        self._conn = conn
-        self._conn_extra_dejson = conn.extra_dejson
+        self._connections[conn.conn_id] = conn
+        self._connection_extra_dejson[conn.conn_id] = conn.extra_dejson
+
+    def _get_provider_kwargs_for_model(
+        self, conn: Connection, model_name: str, extra: dict[str, Any]
+    ) -> dict[str, Any]:
+        provider_name, _ = parse_model_id(model_name)
+        provider_config = _PROVIDER_CONNECTION_CONFIGS.get(provider_name)
+        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:')."
+            )
+        if provider_config is None:
+            return PydanticAIHook._get_provider_kwargs(conn.password, 
conn.host, extra)
+        if provider_config.replacement_fields:
+            ignored_fields = [
+                field for field, value in (("password", conn.password), 
("host", conn.host)) if value
+            ]
+            if ignored_fields:
+                self.log.warning(
+                    "Connection fields are ignored for provider %r on 
connection %r; "
+                    "ignored fields: %s; configure these provider-specific 
values in extra: %s",
+                    provider_name,
+                    conn.conn_id,
+                    ignored_fields,
+                    list(provider_config.replacement_fields),
+                )
+        ignored_extra_fields = [field for field in 
provider_config.ignored_extra_fields if extra.get(field)]
+        if ignored_extra_fields:
+            self.log.warning(
+                "Connection extra fields are ignored for provider %r on 
connection %r: %s",
+                provider_name,
+                conn.conn_id,
+                ignored_extra_fields,
+            )
+        return provider_config.get_kwargs(conn.password, conn.host, extra)
+
+    def _get_provider_factory_for_model(
+        self, conn: Connection, model_name: str, extra: dict[str, Any]
+    ) -> Callable[[str], Any] | None:
+        provider_name, _ = parse_model_id(model_name)
+        if provider_name == "sentence-transformers":
+            return None
+
+        provider_kwargs = self._get_provider_kwargs_for_model(conn, 
model_name, extra)
+        if not provider_kwargs:
+            return None
+
+        self.log.info(
+            "Using explicit connection credentials for model '%s': %s",
+            model_name,
+            list(provider_kwargs),
+        )
+
+        def create_provider(provider: str) -> Any:
+            try:
+                return infer_provider_class(provider)(**provider_kwargs)
+            except TypeError as e:
+                raise TypeError(
+                    f"Provider {provider!r} rejected connection 
{conn.conn_id!r} fields "
+                    f"mapped to kwargs {sorted(provider_kwargs)}: {e}"
+                ) from e
+
+        return create_provider
+
+    def _validate_embedding_connection_provider(self, embed_model_name: str, 
extra: dict[str, Any]) -> None:
+        if self.embed_conn_id != self.llm_conn_id:
+            return
+
+        llm_model_name = self.model_id or extra.get("model", "")
+        if not llm_model_name:
+            return
+
+        llm_provider, _ = parse_model_id(llm_model_name)

Review Comment:
   This one came out of my own suggestion on 
https://github.com/apache/airflow/pull/71437#discussion_r4032377807, so the 
regression is mine as much as yours. The prefixless `embed_model` half is 
right; the `llm_provider is None` arm that came with it skips the credential 
guard altogether.
   
   Whether the guard runs now depends on whether the user typed a prefix that 
the connection pages call optional. Measured against HEAD on pydantic-ai 2.31.1 
with the real provider classes, on a generic `pydanticai` connection holding an 
API key, `model: "gpt-4o"`, `embed_model: "cohere:embed-v4.0"`:
   
   ```
   guard   -> passes (llm_provider is None, early return)
   kwargs  -> {'api_key': 'sk-OPENAI-KEY'}
   embedder-> CohereEmbeddingModel via CohereProvider, 
base_url=https://api.cohere.com
   ```
   
   The OpenAI key is sent to Cohere. Writing `openai:gpt-4o` on that same 
connection raises correctly, and that is the case the guard exists for.
   
   It rejects in the other direction too. A `pydanticai_bedrock` connection 
with Model `us.anthropic.claude-opus-4-6-v1:0`, the bare native id 
`pydantic_ai_bedrock.rst:65-74` documents, colon and all, plus Embedding Model 
`bedrock:amazon.titan-embed-text-v2:0`, is refused with `configures different 
LLM and embedding providers ('us.anthropic.claude-opus-4-6-v1' and 'bedrock')`, 
because `parse_model_id` splits on the id's own colon. Both are Bedrock, 
`get_conn()` resolves that same string to `BedrockConverseModel` behind 
`BedrockProvider`, and the separate `embed_conn_id` the message prescribes is 
not the fix.
   
   Both directions have the same root: this line derives the LLM provider with 
a bare `parse_model_id`, while every other path goes through 
`_qualify_model_name` / `_has_recognized_provider_prefix`, which ignores a `:` 
that is not a real provider name and falls back to `model_provider`. Reusing 
that here, roughly `llm_provider = parse_model_id(llm_model_name)[0] if 
_has_recognized_provider_prefix(llm_model_name) else self.model_provider`, and 
returning early only when it is still `None`, covers both. 
`test_prefixless_llm_model_skips_embedding_provider_validation` pins the 
current arm, and no test sets a bare vendor model name together with an 
`embed_model` on one connection.
   
   While you are in here, the `embed_provider == "sentence-transformers"` arm 
on the next line is unreachable: `_get_provider_factory_for_model` already 
returns `None` for that prefix, and this validator only runs where the factory 
is not `None`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to