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


##########
providers/common/ai/tests/unit/common/ai/test_provider_metadata.py:
##########
@@ -0,0 +1,482 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Drift tripwires between ``provider.yaml``'s declared ``external-services`` and 
what
+pydantic-ai actually installs.
+
+How ``LABELS``, ``NON_MODULE_SERVICES``, ``UNREACHABLE_MODULES``,
+``DEPRECATED_UPSTREAM_MODULES`` and ``HOOK_INCOMPATIBLE_AUTH_MODULES`` below 
were derived,
+including the empirical checks that back each exclusion, is documented in the 
comment directly
+above each constant.
+"""
+
+from __future__ import annotations
+
+import importlib
+import inspect
+import pkgutil
+import re
+from importlib.metadata import version
+from pathlib import Path
+
+import pydantic_ai.providers as pydantic_ai_providers
+import pytest
+import yaml
+from packaging.version import Version
+from pydantic_ai.exceptions import UserError
+from pydantic_ai.models import infer_model, known_model_names
+
+from airflow.providers.common.ai.get_provider_info import get_provider_info
+
+# `toolsets` never flowed through the runtime `provider_info.schema.json`
+# (memory `reference_two_provider_schemas_only_authoring_may_tighten`), so
+# `get_provider_info()` doesn't carry it -- read `provider.yaml` directly 
instead.
+PROVIDER_YAML_PATH = Path(__file__).resolve().parents[4] / "provider.yaml"
+
+
+def _extract_toolset_block() -> dict:
+    provider_yaml = yaml.safe_load(PROVIDER_YAML_PATH.read_text())
+    (toolset_block,) = provider_yaml["toolsets"]
+    return toolset_block
+
+
+# Module basename -> display label used in `provider.yaml`'s 
`pydanticai.external-services`.
+# Sourced from each module's own `name` property / class docstring; kept here 
(not in
+# `src/`) because it is test data, not runtime behaviour.
+LABELS = {
+    "alibaba": "Alibaba Cloud Model Studio",
+    "anthropic": "Anthropic",
+    "azure": "Azure OpenAI",
+    "bedrock": "AWS Bedrock",
+    "bedrock_mantle": "AWS Bedrock Mantle",
+    "cerebras": "Cerebras",
+    "cohere": "Cohere",
+    "crusoe": "Crusoe",
+    "deepseek": "DeepSeek",
+    "fireworks": "Fireworks AI",
+    "gateway": "Pydantic AI Gateway",
+    "github_copilot": "GitHub Copilot",
+    "google": "Google Gemini",
+    "google_cloud": "Google Vertex AI",
+    "groq": "Groq",
+    "heroku": "Heroku",
+    "huggingface": "Hugging Face",
+    "litellm": "LiteLLM",
+    "mistral": "Mistral AI",
+    "moonshotai": "Moonshot AI",
+    "nebius": "Nebius",
+    "ollama": "Ollama",
+    "openai": "OpenAI",
+    "openrouter": "OpenRouter",
+    "ovhcloud": "OVHcloud",
+    "sambanova": "SambaNova",
+    "snowflake": "Snowflake Cortex",
+    "together": "Together AI",
+    "typesafe": "TypeSafe",
+    "vercel": "Vercel AI Gateway",
+    "vllm": "vLLM",
+    "xai": "xAI",
+    "zai": "Z.AI",
+}
+
+# Declared in `pydanticai.external-services` but not backed by a 
`pydantic_ai.providers` module of
+# its own -- reached instead through some other provider's constructor kwargs. 
Currently empty:
+# `vLLM` was the sole member here (reached by pointing the `openai` provider's 
`base_url` at a
+# vLLM endpoint, `hooks/pydantic_ai.py`) until pydantic-ai 2.44.0 added a 
dedicated
+# `pydantic_ai.providers.vllm` module with its own `name` property (`'vllm'`) 
-- see
+# `LABELS["vllm"]` above. Kept, rather than deleted, as where a future vendor 
without its own
+# module would go.
+NON_MODULE_SERVICES: set[str] = set()
+
+# Modules `pkgutil` discovers under `pydantic_ai.providers` that `infer_model` 
never
+# routes to -- `infer_model("voyageai:...")` / 
`infer_model("sentence-transformers:...")`
+# raise `UserError: Unknown model`, confirmed by 
`test_unreachable_modules_stay_unreachable`
+# below. Not listed in `LABELS`/`provider.yaml` because there is no 
`common.ai` code path
+# (chat or embedding) that reaches them.
+UNREACHABLE_MODULES = {"voyageai", "sentence_transformers"}
+
+# Modules that ARE reachable through `infer_model` (unlike 
UNREACHABLE_MODULES) but are
+# excluded by decision because pydantic-ai itself marks the upstream backend 
dead:
+# `pydantic_ai.providers.github.GitHubProvider` is decorated `@deprecated` 
with the message
+# "GitHub Models was retired on 2026-07-30 ... this provider is deprecated and 
will be
+# removed in v3." There is no GitHub Copilot provider anywhere in pydantic-ai; 
the only
+# "github" support that ever existed is this now-retired integration. 
Advertising a vendor
+# whose backend the vendor itself shut down is worse than not listing it 
(Wei's call,
+# 2026-09-11 review round). `test_deprecated_upstream_modules_stay_deprecated` 
below
+# guards the reversal: if pydantic-ai un-deprecates this module, or a real 
GitHub Copilot
+# provider appears under a different module name, the corresponding test goes 
red instead
+# of silently staying excluded.
+DEPRECATED_UPSTREAM_MODULES = {"github"}
+
+# Modules that ARE reachable through `infer_model` (unlike 
UNREACHABLE_MODULES) but are excluded
+# by decision because `PydanticAIHook` has no way to supply the credentials 
they require.
+# `PydanticAIHook._get_provider_kwargs()` -- the generic connection type's 
only credential path
+# (`hooks/pydantic_ai.py`) -- always sends `api_key` (from `conn.password`) 
and/or `base_url`
+# (from `conn.host`). 
`pydantic_ai.providers.openai_codex.OpenAICodexProvider.__init__` accepts
+# only `credentials`, `credential_source`, `openai_client`, and `http_client` 
-- no parameter
+# exists to receive what the hook sends; confirmed empirically (as of 
pydantic-ai-slim 2.44.0):
+# `OpenAICodexProvider(api_key=..., base_url=...)` raises
+# ``TypeError: unexpected keyword argument 'api_key'``. 
`PydanticAIHook.get_conn()`'s
+# `_provider_factory` catches that `TypeError` and retries via 
`infer_provider(pname)`, i.e.
+# `OpenAICodexProvider()` with no arguments, which falls through to its
+# `_read_codex_cli_credentials()` helper: a read-only load of the Codex CLI's 
on-disk
+# `~/.codex/auth.json`, populated by the OAuth authorization-code + PKCE login 
flow the Codex CLI
+# itself performs (see `pydantic_ai.providers.openai_codex`'s module docstring 
and
+# `OpenAICodexOAuthFlow`) -- not by anything `hooks/pydantic_ai.py` can 
express. No `pydanticai`
+# connection field maps to an OAuth credential or a `credential_source`, so a 
`pydanticai`
+# connection can never drive this provider regardless of what's configured on 
it. Advertising a
+# vendor no configuration on this connection type can ever reach is worse than 
not listing it
+# (mirrors `DEPRECATED_UPSTREAM_MODULES`'s reasoning, for a different cause: 
hook-incompatible
+# auth rather than a retired backend). 
`test_hook_incompatible_auth_modules_stay_incompatible`
+# below guards the reversal: if pydantic-ai gives one of these providers an 
`api_key`/`base_url`
+# constructor parameter, this must fail so a human re-evaluates whether the 
module belongs back
+# in `LABELS` and `provider.yaml`, instead of it staying excluded forever on a 
stale rationale.
+HOOK_INCOMPATIBLE_AUTH_MODULES = {"openai_codex"}

Review Comment:
   `get_conn()`'s `_provider_factory` catches the `TypeError` from 
`OpenAICodexProvider(api_key=..., base_url=...)` and retries with no arguments. 
The retry succeeds using the Codex CLI's ambient `~/.codex/auth.json`, not the 
connection's configured credentials. A pydanticai connection pointed at 
openai_codex doesn't fail here. Excluding it from the docs page doesn't prevent 
someone from reaching it this way.



-- 
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