Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e37b0bc
warn about azure openai completions file incompatibility
dsfaccini Jan 20, 2026
91808ff
fix exampels
dsfaccini Jan 20, 2026
7cdf685
move example over to azure
dsfaccini Jan 23, 2026
e6d4f06
fix link
dsfaccini Jan 23, 2026
9e0a102
add test for coverage
dsfaccini Jan 23, 2026
5c45e72
add test for coverage
dsfaccini Jan 23, 2026
cb6aa6b
coverage
dsfaccini Jan 25, 2026
17c6c25
fix test
dsfaccini Jan 25, 2026
63d7e6d
coverage
dsfaccini Jan 25, 2026
91b8337
Merge branch 'main' into review-azure-file-support
dsfaccini Jan 26, 2026
a20068c
Merge branch 'main' into review-azure-file-support
dsfaccini Jan 27, 2026
098f5ec
Merge branch 'main' into review-azure-file-support
dsfaccini Feb 5, 2026
584a99d
Address review: rename file→document, fix docs
dsfaccini Feb 15, 2026
61e4324
Re-record test_yaml_document_url_input cassette
dsfaccini Feb 15, 2026
67c8371
Merge branch 'main' into review-azure-file-support
dsfaccini Feb 15, 2026
caec194
Add SSRF fixture to test_yaml_document_url_input and re-record cassette
dsfaccini Feb 15, 2026
ee4a419
Add tests for DocumentUrl path in document input not supported error
dsfaccini Feb 15, 2026
b663951
address review feedback: remove symlink, fix docs table, update model…
dsfaccini Feb 27, 2026
aff00f3
Merge remote-tracking branch 'upstream/main' into review-azure-file-s…
dsfaccini Mar 19, 2026
22d356d
Update docs/models/openai.md
dsfaccini Mar 19, 2026
a10f9f1
remove unnecessary backticks
dsfaccini Mar 19, 2026
f882fe8
prepush review
dsfaccini Mar 26, 2026
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
28 changes: 27 additions & 1 deletion docs/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,33 @@ Support for file URLs varies depending on type and provider:
| [`MistralModel`][pydantic_ai.models.mistral.MistralModel] | `ImageUrl`, `DocumentUrl` (PDF) | — | `AudioUrl`, `VideoUrl`, `DocumentUrl` (non-PDF) |
| [`BedrockConverseModel`][pydantic_ai.models.bedrock.BedrockConverseModel] | S3 URLs (`s3://`) | `ImageUrl`, `DocumentUrl`, `VideoUrl` | `AudioUrl` |

A model API may be unable to download a file (e.g., because of crawling or access restrictions) even if it supports file URLs. For example, [`GoogleModel`][pydantic_ai.models.google.GoogleModel] on Vertex AI limits YouTube video URLs to one URL per request. In such cases, you can instruct Pydantic AI to download the file content locally and send that instead of the URL by setting `force_download` on the URL object:
??? warning "`DocumentUrl` and `BinaryContent` documents are not supported when using `AzureProvider` with `OpenAIChatModel`."
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather add a Notes column to the table, like we have on the builtin tools doc, where we can state on the OpenAIChatModel row that Azure doesn't support it, and any other notes for provider-specific support (I believe there are some profile flags for specific providers that enable file types that are not supported by OpenAI itself)

Copy link
Copy Markdown
Collaborator Author

@dsfaccini dsfaccini Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking at my screen I'm not liking the idea of a fifth column
image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a fifth column, what about mentioning the Azure case under "Unsupported", and then linking to the section that explains the responses workaround?

Use [`OpenAIResponsesModel`][pydantic_ai.models.openai.OpenAIResponsesModel] with [`AzureProvider`][pydantic_ai.providers.azure.AzureProvider] instead:
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need an example here, but I want to make sure the Azure doc (currently section on the OpenAI page) explains (in a generic way) that it can be used with the Responses API as well, and that that's actually recommend over OpenAIChatModel. Can you add it there instead please?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure, TODO

Copy link
Copy Markdown
Collaborator Author

@dsfaccini dsfaccini Jan 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to azure, I'm thinking we should simply replace the default example with responses and leave the chat one in a second tab?
Edit: also can I remove those trailing ... from examples?
image


```python
from pydantic_ai import Agent, BinaryContent
from pydantic_ai.models.openai import OpenAIResponsesModel
from pydantic_ai.providers.azure import AzureProvider

pdf_bytes = b'%PDF-1.4 ...' # Your PDF content

model = OpenAIResponsesModel(
'gpt-5',
provider=AzureProvider(
azure_endpoint='your-azure-endpoint',
api_version='your-api-version',
),
)
agent = Agent(model)
result = agent.run_sync([
'Summarize this document',
BinaryContent(data=pdf_bytes, media_type='application/pdf'),
])
```

A model API may be unable to download a file (e.g., because of crawling or access restrictions) even if it supports file URLs. For example, [`GoogleModel`][pydantic_ai.models.google.GoogleModel] on Vertex AI limits YouTube video URLs to one URL per request.

In such cases, you can instruct Pydantic AI to download the file content locally and send that instead of the URL by setting `force_download` on the URL object:

```py {title="force_download.py" test="skip" lint="skip"}
from pydantic_ai import ImageUrl, AudioUrl, VideoUrl, DocumentUrl
Expand Down
16 changes: 15 additions & 1 deletion pydantic_ai_slim/pydantic_ai/models/openai.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Potential bypass of document support check when openai_chat_supports_file_urls=True

In _map_document_url_item at pydantic_ai_slim/pydantic_ai/models/openai.py:1245, the first branch checks not item.force_download and profile.openai_chat_supports_file_urls and returns a File content part directly WITHOUT checking openai_chat_supports_document_input. If a provider were configured with openai_chat_supports_file_urls=True AND openai_chat_supports_document_input=False, the document support check would be bypassed. Currently no provider has this combination (only OpenRouter sets openai_chat_supports_file_urls=True, and it supports documents), so this is not a practical issue today. But it's a latent inconsistency that could matter if a new provider is added with this combination.

(Refers to line 1245)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the openai_chat_supports_file_urls flag inherently implies the provider supports documents, so I don't think we need to explicitly handle the combination

for future reference, if we found these inconsistencies, having a single dataclass that uses property to validate combinations would be a better approach than checking multiple flags, since adding a check branch here would bloat this out further

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from pydantic import BaseModel, ValidationError
from pydantic_core import to_json
from typing_extensions import assert_never, deprecated
from typing_extensions import Never, assert_never, deprecated

from .. import ModelAPIError, ModelHTTPError, UnexpectedModelBehavior, _utils, usage
from .._output import DEFAULT_OUTPUT_TOOL_NAME, OutputObjectDefinition
Expand Down Expand Up @@ -1087,6 +1087,8 @@ async def _map_user_prompt(self, part: UserPromptPart) -> chat.ChatCompletionUse
audio = InputAudio(data=item.base64, format=item.format)
content.append(ChatCompletionContentPartInputAudioParam(input_audio=audio, type='input_audio'))
elif item.is_document:
if not profile.openai_chat_supports_file_input:
self._raise_file_input_not_supported_error()
content.append(
File(
file=FileFile(
Expand Down Expand Up @@ -1118,6 +1120,8 @@ async def _map_user_prompt(self, part: UserPromptPart) -> chat.ChatCompletionUse
)
)
else:
if not profile.openai_chat_supports_file_input:
self._raise_file_input_not_supported_error()
downloaded_item = await download_item(item, data_format='base64_uri', type_format='extension')
content.append(
File(
Expand All @@ -1137,6 +1141,16 @@ async def _map_user_prompt(self, part: UserPromptPart) -> chat.ChatCompletionUse
assert_never(item)
return chat.ChatCompletionUserMessageParam(role='user', content=content)

def _raise_file_input_not_supported_error(self) -> Never:
if self._provider.name == 'azure':
raise UserError(
"Azure's Chat Completions API does not support document input. "
'Use `OpenAIResponsesModel` with `AzureProvider` instead.'
)
raise UserError(
f'The {self._provider.name!r} provider does not support document input via the Chat Completions API.'
)

@staticmethod
def _is_text_like_media_type(media_type: str) -> bool:
return (
Expand Down
7 changes: 7 additions & 0 deletions pydantic_ai_slim/pydantic_ai/profiles/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ class OpenAIModelProfile(ModelProfile):
See https://github.com/pydantic/pydantic-ai/issues/3245 for more details.
"""

openai_chat_supports_file_input: bool = True
"""Whether the Chat Completions API supports file content parts (type='file').

Some OpenAI-compatible providers (e.g. Azure) do not support file input via the Chat Completions API.
For Azure, use `OpenAIResponsesModel` with `AzureProvider` instead.
"""

def __post_init__(self): # pragma: no cover
if not self.openai_supports_sampling_settings:
warnings.warn(
Expand Down
9 changes: 7 additions & 2 deletions pydantic_ai_slim/pydantic_ai/providers/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ def model_profile(self, model_name: str) -> ModelProfile | None:

# As AzureProvider is always used with OpenAIChatModel, which used to unconditionally use OpenAIJsonSchemaTransformer,
# we need to maintain that behavior unless json_schema_transformer is set explicitly
return OpenAIModelProfile(json_schema_transformer=OpenAIJsonSchemaTransformer).update(profile)
# Azure Chat Completions API doesn't support file input
return OpenAIModelProfile(
json_schema_transformer=OpenAIJsonSchemaTransformer,
openai_chat_supports_file_input=False,
).update(profile)

# OpenAI models are unprefixed
return openai_model_profile(model_name)
# Azure Chat Completions API doesn't support file input
return OpenAIModelProfile(openai_chat_supports_file_input=False).update(openai_model_profile(model_name))

@overload
def __init__(self, *, openai_client: AsyncAzureOpenAI) -> None: ...
Expand Down
23 changes: 23 additions & 0 deletions tests/providers/test_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from inline_snapshot import snapshot
from pytest_mock import MockerFixture

from pydantic_ai import BinaryContent
from pydantic_ai._json_schema import InlineDefsJsonSchemaTransformer
from pydantic_ai.agent import Agent
from pydantic_ai.exceptions import UserError
from pydantic_ai.profiles.cohere import cohere_model_profile
from pydantic_ai.profiles.deepseek import deepseek_model_profile
from pydantic_ai.profiles.grok import grok_model_profile
Expand Down Expand Up @@ -139,3 +141,24 @@ def test_azure_provider_model_profile(mocker: MockerFixture):
openai_model_profile_mock.assert_called_with('unknown-model')
assert unknown_profile is not None
assert unknown_profile.json_schema_transformer == OpenAIJsonSchemaTransformer


async def test_azure_document_input_not_supported(allow_model_requests: None):
provider = AzureProvider(
azure_endpoint='https://project-id.openai.azure.com/',
api_version='2023-03-15-preview',
api_key='1234567890',
)
model = OpenAIChatModel(model_name='gpt-4o', provider=provider)
agent = Agent(model)

with pytest.raises(
UserError,
match="Azure's Chat Completions API does not support document input.*OpenAIResponsesModel",
):
await agent.run(
[
'Summarize this document',
BinaryContent(data=b'%PDF-1.4 test', media_type='application/pdf'),
]
)
2 changes: 2 additions & 0 deletions tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,8 @@ async def model_logic( # noqa: C901
return ModelResponse(parts=[TextPart('The company name in the logo is "Pydantic."')])
elif isinstance(m.content, list) and m.content[0] == 'This is file c6720d:':
return ModelResponse(parts=[TextPart('The document contains just the text "Dummy PDF file."')])
elif isinstance(m.content, list) and m.content[0] == 'Summarize this document':
return ModelResponse(parts=[TextPart('This document outlines the PDF specification version 1.4.')])

assert isinstance(m.content, str)
if m.content == 'Tell me a joke.' and any(t.name == 'joke_factory' for t in info.function_tools):
Expand Down
Loading