Skip to content

Add Azure Provider #1091

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@
::: pydantic_ai.providers.bedrock

::: pydantic_ai.providers.groq

::: pydantic_ai.providers.azure
22 changes: 22 additions & 0 deletions docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,28 @@ Usage(requests=1, request_tokens=57, response_tokens=8, total_tokens=65, details
1. The name of the model running on the remote server
2. The url of the remote server

### Azure AI Foundry

If you want to use [Azure AI Foundry](https://ai.azure.com/) as your provider, you can do so by using the
[`AzureProvider`][pydantic_ai.providers.azure.AzureProvider] class.

```python {title="azure_provider_example.py"}
from pydantic_ai import Agent
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.azure import AzureProvider

model = OpenAIModel(
'gpt-4o',
provider=AzureProvider(
azure_endpoint='your-azure-endpoint',
api_version='your-api-version',
api_key='your-api-key',
),
)
agent = Agent(model)
...
```

### OpenRouter

To use [OpenRouter](https://openrouter.ai), first create an API key at [openrouter.ai/keys](https://openrouter.ai/keys).
Expand Down
4 changes: 2 additions & 2 deletions pydantic_ai_slim/pydantic_ai/models/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def __init__(
self,
model_name: OpenAIModelName,
*,
provider: Literal['openai', 'deepseek'] | Provider[AsyncOpenAI] = 'openai',
provider: Literal['openai', 'deepseek', 'azure'] | Provider[AsyncOpenAI] = 'openai',
system_prompt_role: OpenAISystemPromptRole | None = None,
system: str | None = 'openai',
) -> None: ...
Expand All @@ -130,7 +130,7 @@ def __init__(
self,
model_name: OpenAIModelName,
*,
provider: Literal['openai', 'deepseek'] | Provider[AsyncOpenAI] | None = None,
provider: Literal['openai', 'deepseek', 'azure'] | Provider[AsyncOpenAI] | None = None,
base_url: str | None = None,
api_key: str | None = None,
openai_client: AsyncOpenAI | None = None,
Expand Down
108 changes: 108 additions & 0 deletions pydantic_ai_slim/pydantic_ai/providers/azure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from __future__ import annotations as _annotations

import os
from typing import overload

import httpx
from openai import AsyncOpenAI

from pydantic_ai.models import cached_async_http_client

try:
from openai import AsyncAzureOpenAI
except ImportError as _import_error: # pragma: no cover
raise ImportError(
'Please install the `openai` package to use the Azure provider, '
"you can use the `openai` optional group — `pip install 'pydantic-ai-slim[openai]'`"
) from _import_error


from . import Provider


class AzureProvider(Provider[AsyncOpenAI]):
"""Provider for Azure OpenAI API.

See <https://azure.microsoft.com/en-us/products/ai-foundry> for more information.
"""

@property
def name(self) -> str:
return 'azure'

@property
def base_url(self) -> str:
assert self._base_url is not None
return self._base_url

@property
def client(self) -> AsyncOpenAI:
return self._client

@overload
def __init__(self, *, openai_client: AsyncAzureOpenAI) -> None: ...

@overload
def __init__(
self,
*,
azure_endpoint: str | None = None,
api_version: str | None = None,
api_key: str | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None: ...

def __init__(
self,
*,
azure_endpoint: str | None = None,
api_version: str | None = None,
api_key: str | None = None,
openai_client: AsyncAzureOpenAI | None = None,
http_client: httpx.AsyncClient | None = None,
) -> None:
"""Create a new Azure provider.

Args:
azure_endpoint: The Azure endpoint to use for authentication, if not provided, the `AZURE_OPENAI_ENDPOINT`
environment variable will be used if available.
api_version: The API version to use for authentication, if not provided, the `OPENAI_API_VERSION`
environment variable will be used if available.
api_key: The API key to use for authentication, if not provided, the `AZURE_OPENAI_API_KEY` environment variable
will be used if available.
openai_client: An existing
[`AsyncAzureOpenAI`](https://github.com/openai/openai-python#microsoft-azure-openai)
client to use. If provided, `base_url`, `api_key`, and `http_client` must be `None`.
http_client: An existing `httpx.AsyncClient` to use for making HTTP requests.
"""
if openai_client is not None:
assert azure_endpoint is None, 'Cannot provide both `openai_client` and `azure_endpoint`'
assert http_client is None, 'Cannot provide both `openai_client` and `http_client`'
assert api_key is None, 'Cannot provide both `openai_client` and `api_key`'
self._base_url = str(openai_client.base_url)
self._client = openai_client
else:
azure_endpoint = azure_endpoint or os.getenv('AZURE_OPENAI_ENDPOINT')
if azure_endpoint is None: # pragma: no cover
raise ValueError(
'Must provide one of the `azure_endpoint` argument or the `AZURE_OPENAI_ENDPOINT` environment variable'
)

if api_key is None and 'OPENAI_API_KEY' not in os.environ: # pragma: no cover
raise ValueError(
'Must provide one of the `api_key` argument or the `OPENAI_API_KEY` environment variable'
)

if api_version is None and 'OPENAI_API_VERSION' not in os.environ: # pragma: no cover
raise ValueError(
'Must provide one of the `api_version` argument or the `OPENAI_API_VERSION` environment variable'
)

http_client = http_client or cached_async_http_client()
self._client = AsyncAzureOpenAI(
azure_endpoint=azure_endpoint,
api_key=api_key,
api_version=api_version,
http_client=http_client,
)
self._base_url = str(self._client.base_url)
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/providers/bedrock.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from botocore.exceptions import NoRegionError
except ImportError as _import_error:
raise ImportError(
'Please install `boto3` to use the Bedrock provider, '
'Please install the `boto3` package to use the Bedrock provider, '
"you can use the `bedrock` optional group — `pip install 'pydantic-ai-slim[bedrock]'`"
) from _import_error

Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/providers/deepseek.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from openai import AsyncOpenAI
except ImportError as _import_error: # pragma: no cover
raise ImportError(
'Please install `openai` to use the DeepSeek provider, '
'Please install the `openai` package to use the DeepSeek provider, '
"you can use the `openai` optional group — `pip install 'pydantic-ai-slim[openai]'`"
) from _import_error

Expand Down
2 changes: 1 addition & 1 deletion pydantic_ai_slim/pydantic_ai/providers/google_vertex.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from google.oauth2.service_account import Credentials as ServiceAccountCredentials
except ImportError as _import_error:
raise ImportError(
'Please install `google-auth` to use the Google Vertex AI provider, '
'Please install the `google-auth` package to use the Google Vertex AI provider, '
"you can use the `vertexai` optional group — `pip install 'pydantic-ai-slim[vertexai]'`"
) from _import_error

Expand Down
4 changes: 1 addition & 3 deletions pydantic_ai_slim/pydantic_ai/providers/groq.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from groq import AsyncGroq
except ImportError as _import_error: # pragma: no cover
raise ImportError(
'Please install `groq` to use the Groq provider, '
'Please install the `groq` package to use the Groq provider, '
"you can use the `groq` optional group — `pip install 'pydantic-ai-slim[groq]'`"
) from _import_error

Expand Down Expand Up @@ -66,8 +66,6 @@ def __init__(
)

if groq_client is not None:
assert http_client is None, 'Cannot provide both `groq_client` and `http_client`'
assert api_key is None, 'Cannot provide both `groq_client` and `api_key`'
self._client = groq_client
elif http_client is not None:
self._client = AsyncGroq(base_url=self.base_url, api_key=api_key, http_client=http_client)
Expand Down
5 changes: 1 addition & 4 deletions pydantic_ai_slim/pydantic_ai/providers/openai.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import annotations as _annotations

import os
from typing import TypeVar

import httpx

Expand All @@ -11,15 +10,13 @@
from openai import AsyncOpenAI
except ImportError as _import_error: # pragma: no cover
raise ImportError(
'Please install `openai` to use the OpenAI provider, '
'Please install the `openai` package to use the OpenAI provider, '
"you can use the `openai` optional group — `pip install 'pydantic-ai-slim[openai]'`"
) from _import_error


from . import Provider

InterfaceClient = TypeVar('InterfaceClient')


class OpenAIProvider(Provider[AsyncOpenAI]):
"""Provider for OpenAI API."""
Expand Down
2 changes: 1 addition & 1 deletion tests/json_body_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from yaml import Dumper, Loader

FILTERED_HEADER_PREFIXES = ['anthropic-', 'cf-', 'x-']
FILTERED_HEADERS = {'authorization', 'date', 'request-id', 'server', 'user-agent', 'via', 'set-cookie'}
FILTERED_HEADERS = {'authorization', 'date', 'request-id', 'server', 'user-agent', 'via', 'set-cookie', 'api-key'}


class LiteralDumper(Dumper):
Expand Down
107 changes: 107 additions & 0 deletions tests/providers/cassettes/test_azure/test_azure_provider_call.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
interactions:
- request:
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
connection:
- keep-alive
content-length:
- '111'
content-type:
- application/json
host:
- pydanticai7521574644.openai.azure.com
method: POST
parsed_body:
messages:
- content: What is the capital of France?
role: user
model: gpt-4o
n: 1
stream: false
uri: https://pydanticai7521574644.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-12-01-preview
response:
headers:
apim-request-id:
- 1d93fae1-cb8e-4789-8fb6-d26577a3cb77
azureml-model-session:
- v20250225-1-161802030
cmp-upstream-response-duration:
- '235'
content-length:
- '1223'
content-type:
- application/json
ms-azureml-model-time:
- '315'
strict-transport-security:
- max-age=31536000; includeSubDomains; preload
parsed_body:
choices:
- content_filter_results:
hate:
filtered: false
severity: safe
protected_material_code:
detected: false
filtered: false
protected_material_text:
detected: false
filtered: false
self_harm:
filtered: false
severity: safe
sexual:
filtered: false
severity: safe
violence:
filtered: false
severity: safe
finish_reason: stop
index: 0
logprobs: null
message:
content: The capital of France is **Paris**.
refusal: null
role: assistant
created: 1741880483
id: chatcmpl-BAeyRj7gU6aCNSSAskAFbupBWYMIT
model: gpt-4o-2024-11-20
object: chat.completion
prompt_filter_results:
- content_filter_results:
hate:
filtered: false
severity: safe
jailbreak:
detected: false
filtered: false
self_harm:
filtered: false
severity: safe
sexual:
filtered: false
severity: safe
violence:
filtered: false
severity: safe
prompt_index: 0
system_fingerprint: fp_ded0d14823
usage:
completion_tokens: 9
completion_tokens_details:
accepted_prediction_tokens: 0
audio_tokens: 0
reasoning_tokens: 0
rejected_prediction_tokens: 0
prompt_tokens: 14
prompt_tokens_details:
audio_tokens: 0
cached_tokens: 0
total_tokens: 23
status:
code: 200
message: OK
version: 1
Loading