added azureopenai support & extandable architecture for pydanticai#62816
added azureopenai support & extandable architecture for pydanticai#62816cetingokhan wants to merge 1 commit intoapache:mainfrom
Conversation
kaxil
left a comment
There was a problem hiding this comment.
Thanks for the contribution! Adding Azure OpenAI support is a good idea.
My main concern: pydantic-ai already ships with native AzureProvider support — see the docs. You can do:
from pydantic_ai.providers.azure import AzureProvider
model = OpenAIChatModel(
'gpt-5.2',
provider=AzureProvider(
azure_endpoint='https://myresource.openai.azure.com',
api_version='2024-07-01-preview',
api_key='...',
),
)Or even just use "azure:gpt-5.2" as the model string (with env vars set). So we don't need to manually construct AsyncAzureOpenAI clients from the openai SDK — pydantic-ai handles Azure natively.
The current hook's get_conn() is ~25 lines and delegates to pydantic-ai's infer_model() + provider_factory. Azure support could be a simple conditional branch using AzureProvider, without the 4-file builder pattern. The whole point of using pydantic-ai is that it abstracts provider differences for us — we should lean on that rather than wrapping it in another layer.
Specific comments inline.
| from pydantic_ai.models import KnownModelName, Model | ||
|
|
||
|
|
||
| class ProviderBuilder(Protocol): |
There was a problem hiding this comment.
Do we need this abstraction? The current get_conn() is a straightforward if/else that delegates to pydantic-ai's own infer_model(). Adding a Protocol + 3 builder classes + a dispatch loop for what's essentially a single new code path (Azure) feels like premature abstraction for a problem that doesn't exist yet. If/when we genuinely need pluggable resolution, we can introduce it then.
Also — ProviderBuilder is declared as a Protocol (structural typing), but the concrete classes inherit from it (nominal typing). These are two different patterns — if you want an inheritance hierarchy, use ABC; if you want duck typing, don't inherit from the Protocol in the concrete classes. Mixing both is confusing for contributors.
| base_url: str | None, | ||
| ) -> Model: | ||
| try: | ||
| from openai import AsyncAzureOpenAI |
There was a problem hiding this comment.
pydantic-ai already has native Azure support via AzureProvider — no need to drop down to the raw openai SDK:
from pydantic_ai.providers.azure import AzureProvider
from pydantic_ai.models.openai import OpenAIChatModel
provider = AzureProvider(
azure_endpoint=base_url,
api_version=api_version,
api_key=api_key,
)
model = OpenAIChatModel(model_name, provider=provider)See https://ai.pydantic.dev/models/openai/#azure
By constructing AsyncAzureOpenAI directly we're bypassing pydantic-ai's own provider abstraction (which may handle retries, error mapping, etc.) and coupling ourselves to openai SDK internals.
Using AzureProvider would also make a separate builder class unnecessary — it could just be a few lines in get_conn().
| return OpenAIChatModel(slug, provider=OpenAIProvider(openai_client=azure_client)) | ||
|
|
||
| @staticmethod | ||
| def _import_callable(dotted_path: str) -> Any: |
There was a problem hiding this comment.
This calls importlib.import_module() on a user-provided string from connection extras — module imports can run arbitrary code at import time. Connection extras are editable by any user with connection-edit permissions, which is a lower-privilege surface than DAG deployment.
If we end up needing a custom token provider path, this should at least be documented as a security-sensitive field. But since pydantic-ai's AzureProvider accepts api_key directly, we may not need this at all for the initial implementation.
|
|
||
| self._model = infer_model(model_name, provider_factory=_provider_factory) | ||
| return self._model | ||
| raise RuntimeError("No suitable ProviderBuilder found to construct the model.") |
There was a problem hiding this comment.
This line is unreachable — DefaultBuilder.supports() always returns True, so the loop will always match on the third iteration. Dead code that implies the loop might not match, which could mislead future readers.
| return _factory | ||
|
|
||
|
|
||
| class DefaultBuilder(ProviderBuilder): |
There was a problem hiding this comment.
DefaultBuilder.build() is return infer_model(model_name) — a single call. The existing hook does this in two lines: if not api_key and not base_url: return infer_model(model_name). Does this one-liner need its own class with Protocol conformance and a supports() method?
|
Thanks for the comments. |
Guide AI coding tools and contributors toward the right design decisions: delegate to pydantic-ai instead of re-implementing provider-specific logic, keep the hook thin, avoid premature abstractions like builder patterns or registries. Motivated by PR apache#62816 which added ~280 lines of Azure OpenAI builder code that duplicated what pydantic-ai's AzureProvider already handles natively.
This pull request refactors the model resolution logic in the
PydanticAIHookto use a modular builder pattern, improving extensibility and clarity for handling different AI providers, especially Azure OpenAI. It introduces dedicated builder classes for Azure OpenAI, custom endpoints, and default resolution, and updates documentation and tests to match the new structure.Builder pattern for model resolution:
AzureOpenAIBuilder,CustomEndpointBuilder,DefaultBuilder, and aProviderBuilderprotocol in thebuilderspackage to modularize how models are constructed from Airflow connection details. [1] [2] [3] [4]Refactoring in
PydanticAIHook:infer_modeland provider factory logic with a prioritized builder selection process (AzureOpenAIBuilder→CustomEndpointBuilder→DefaultBuilder), improving support for Azure OpenAI and custom endpoints.Documentation and UI improvements:
PydanticAIHookto clarify connection fields for Azure OpenAI and provide examples for required extras likeapi_versionandazure_deployment. [1] [2]Testing updates:
infer_modelandinfer_provider_classfrom the new builder modules instead of the hook, ensuring tests match the refactored code structure. [1] [2] [3] [4] [5]Connection validation enhancements:
api_versionandhost) and clarified error handling for missing model configuration.Was generative AI tooling used to co-author this PR?
Cloude Sonnet 4.6 & Gemini 3.1 Pro
Filled some of methods scope and tests created via copilot
{pr_number}.significant.rstor{issue_number}.significant.rst, in airflow-core/newsfragments.