This is an automated email from the ASF dual-hosted git repository.
kaxil 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 f34cae8fa6f Suggest common model providers in the pydanticai
connection form (#76053)
f34cae8fa6f is described below
commit f34cae8fa6f59903d75f0db4d3a358ce4e4ea6f7
Author: Kaxil Naik <[email protected]>
AuthorDate: Wed Nov 4 01:41:02 2026 +0000
Suggest common model providers in the pydanticai connection form (#76053)
The Default Model Provider field was a blank text box, so users had to know
pydantic-ai's provider names (`openai-chat` vs `openai`) by heart. The
field's
schema now lists common ones as `examples`, which the connection form
renders as
suggestions on a text input: any other provider pydantic-ai recognizes can
still
be typed. An `enum` dropdown would reject providers pydantic-ai adds until
a new
release of this provider. A test keeps every suggestion resolvable.
The suggestions ship with the provider, but the installed pydantic-ai can
recognize more. The field's help text and the connection docs now say the
list
is common choices, not every provider that works. A test also keeps each
suggestion among the connection's declared external services, so the form
never
suggests a vendor the docs and registry don't list.
* Keep the model provider help text accurate on the registry and share test
lookups
The registry's connection builder shows a conn-field's description but not
its
examples, so the help text now says any provider the installed pydantic-ai
recognizes works instead of describing suggestions. The docs paragraph no
longer
repeats part of the list by hand. The suggestion checks reuse one
connection-type
lookup and one prefix-to-module mapping, and the two pydantic-ai checks on
this
field are one test.
---
.../common/ai/docs/connections/pydantic_ai.rst | 4 +++
providers/common/ai/provider.yaml | 18 +++++++++++++-
.../providers/common/ai/get_provider_info.py | 19 ++++++++++++--
.../tests/unit/common/ai/hooks/test_pydantic_ai.py | 14 ++++++-----
.../tests/unit/common/ai/test_provider_metadata.py | 29 +++++++++++++++++++---
5 files changed, 71 insertions(+), 13 deletions(-)
diff --git a/providers/common/ai/docs/connections/pydantic_ai.rst
b/providers/common/ai/docs/connections/pydantic_ai.rst
index 9ff9ac13da2..831e0ac2b54 100644
--- a/providers/common/ai/docs/connections/pydantic_ai.rst
+++ b/providers/common/ai/docs/connections/pydantic_ai.rst
@@ -91,6 +91,10 @@ Default Model Provider
``@task.llm_batch`` applies it the same way, and sends ``openai-chat``
models
to its OpenAI adapter.
+ The form suggests common providers as you type. The suggestions are not a
full
+ list: any provider the installed pydantic-ai recognizes is accepted,
including
+ ones a newer pydantic-ai adds.
+
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
diff --git a/providers/common/ai/provider.yaml
b/providers/common/ai/provider.yaml
index eb370d97f32..7196a6955d1 100644
--- a/providers/common/ai/provider.yaml
+++ b/providers/common/ai/provider.yaml
@@ -232,11 +232,27 @@ connection-types:
- 'null'
model_provider:
label: Default Model Provider
- description: "Provider for model names without a provider: prefix
(e.g. openai-chat)"
+ description: >
+ Provider for model names without a provider: prefix (e.g.
openai-chat).
+ Any provider the installed pydantic-ai recognizes works.
schema:
type:
- string
- 'null'
+ # Suggestions on a free-text input, not an enum: an enum would
reject providers a newer
+ # pydantic-ai adds until the next release of this provider.
+ examples:
+ - openai-chat
+ - openai
+ - anthropic
+ - ollama
+ - litellm
+ - openrouter
+ - groq
+ - mistral
+ - deepseek
+ - together
+ - fireworks
fallback_conn_ids:
label: Fallback Connections
description: "Connection IDs to fail over to, in order, while this
provider is unavailable."
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 85112a45c33..ffea5c04250 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
@@ -201,8 +201,23 @@ def get_provider_info():
},
"model_provider": {
"label": "Default Model Provider",
- "description": "Provider for model names without a
provider: prefix (e.g. openai-chat)",
- "schema": {"type": ["string", "null"]},
+ "description": "Provider for model names without a
provider: prefix (e.g. openai-chat). Any provider the installed pydantic-ai
recognizes works.\n",
+ "schema": {
+ "type": ["string", "null"],
+ "examples": [
+ "openai-chat",
+ "openai",
+ "anthropic",
+ "ollama",
+ "litellm",
+ "openrouter",
+ "groq",
+ "mistral",
+ "deepseek",
+ "together",
+ "fireworks",
+ ],
+ },
},
"fallback_conn_ids": {
"label": "Fallback Connections",
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 04f8ebb0813..47af1321456 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
@@ -2325,16 +2325,18 @@ class TestPydanticAIVertexHook:
prefix = _extract_google_cloud_prefix(description)
_assert_prefix_is_known_provider(prefix)
- def
test_conn_fields_default_provider_description_example_is_valid_provider(self):
- """Drift tripwire for the example in the generic connection's Default
Model Provider conn-field."""
+ def
test_conn_fields_default_provider_example_and_suggestions_are_valid_providers(self):
+ """Drift tripwire for the Default Model Provider description example
and form suggestions."""
connection_types = get_provider_info()["connection-types"]
generic_conn_fields = next(
c["conn-fields"] for c in connection_types if c["connection-type"]
== "pydanticai"
)
- description = generic_conn_fields["model_provider"]["description"]
- example = re.search(r"\(e\.g\. ([a-z0-9-]+)\)", description)
- assert example is not None, description
- _assert_prefix_is_known_provider(example.group(1))
+ field = generic_conn_fields["model_provider"]
+ example = re.search(r"\(e\.g\. ([a-z0-9-]+)\)", field["description"])
+ assert example is not None, field["description"]
+ assert field["schema"]["examples"]
+ for provider in [example.group(1), *field["schema"]["examples"]]:
+ _assert_prefix_is_known_provider(provider)
def
test_conn_types_ui_field_behaviour_placeholder_prefix_is_valid_provider(self):
"""
diff --git a/providers/common/ai/tests/unit/common/ai/test_provider_metadata.py
b/providers/common/ai/tests/unit/common/ai/test_provider_metadata.py
index df10e78b415..16ae1309350 100644
--- a/providers/common/ai/tests/unit/common/ai/test_provider_metadata.py
+++ b/providers/common/ai/tests/unit/common/ai/test_provider_metadata.py
@@ -267,10 +267,19 @@ def _provider_classes_in_module(mod) -> list[type]:
return provider_classes
-def _find_pydanticai_external_services() -> list[str]:
+def _find_pydanticai_connection_type() -> dict:
connection_types = get_provider_info()["connection-types"]
(pydanticai,) = (c for c in connection_types if c["connection-type"] ==
"pydanticai")
- return pydanticai["external-services"]
+ return pydanticai
+
+
+def _find_pydanticai_external_services() -> list[str]:
+ return _find_pydanticai_connection_type()["external-services"]
+
+
+def _module_for_prefix(prefix: str) -> str:
+ """The pydantic-ai provider module behind a model-name prefix
(``openai-chat`` -> ``openai``)."""
+ return PREFIX_ALIASES.get(prefix, prefix.replace("-", "_"))
class TestConnectionTypeExternalServices:
@@ -281,6 +290,18 @@ class TestConnectionTypeExternalServices:
)
+class TestModelProviderSuggestions:
+ def test_every_suggestion_is_a_declared_external_service(self):
+ """The form must not suggest a vendor the docs and registry don't list
for this connection."""
+ pydanticai = _find_pydanticai_connection_type()
+ suggestions =
pydanticai["conn-fields"]["model_provider"]["schema"]["examples"]
+ declared = set(pydanticai["external-services"])
+
+ undeclared = [s for s in suggestions if
LABELS.get(_module_for_prefix(s)) not in declared]
+
+ assert not undeclared, f"suggested providers missing from
external-services: {undeclared}"
+
+
class TestPydanticAIExternalServicesDrift:
def test_every_installed_vendor_is_declared(self):
"""
@@ -381,7 +402,7 @@ class TestPydanticAIExternalServicesDrift:
if ":" not in name:
continue # the bare "test" entry, pydantic-ai's built-in
keyless TestModel
prefix = name.removeprefix("gateway/").split(":", 1)[0]
- module = PREFIX_ALIASES.get(prefix, prefix.replace("-", "_"))
+ module = _module_for_prefix(prefix)
assert module in labelled, f"model prefix {prefix!r} (from
{name!r}) has no label"
def test_gateway_note_vendors_match_known_model_names(self):
@@ -396,7 +417,7 @@ class TestPydanticAIExternalServicesDrift:
for name in known_model_names()
if name.startswith("gateway/")
}
- modules = {PREFIX_ALIASES.get(p, p.replace("-", "_")) for p in
gateway_prefixes}
+ modules = {_module_for_prefix(p) for p in gateway_prefixes}
derived = {LABELS.get(m, f"<unlabelled:{m}>") for m in modules}
assert derived == GATEWAY_NOTE_VENDOR_LABELS, (