Skip to content

Commit e873dd2

Browse files
gaurav0107adtyavrdhnadtyavrdhn
authored
Auto-enable Anthropic Files API beta when messages contain UploadedFile (#5815)
Co-authored-by: Aditya Vardhan <76904033+adtyavrdhn@users.noreply.github.com> Co-authored-by: adtyavrdhn <aditya@pydantic.dev>
1 parent ab79636 commit e873dd2

8 files changed

Lines changed: 392 additions & 103 deletions

‎docs/input.md‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,12 +218,12 @@ The `media_type` parameter is optional for [`UploadedFile`][pydantic_ai.messages
218218
Follow the [Anthropic Files API docs](https://docs.anthropic.com/en/docs/build-with-claude/files) to upload files. You can access the underlying Anthropic client via `provider.client`.
219219

220220
!!! note "Beta Feature"
221-
The Anthropic Files API is currently in beta. You need to include the beta header `anthropic-beta: files-api-2025-04-14` when making requests.
221+
The Anthropic Files API is currently in beta. `AnthropicModel` automatically adds the required `anthropic-beta: files-api-2025-04-14` header when a request contains an Anthropic [`UploadedFile`][pydantic_ai.messages.UploadedFile], so you don't need to set it yourself.
222222

223223
```py {title="uploaded_file_anthropic.py" test="skip"}
224224
import asyncio
225225

226-
from pydantic_ai import Agent, ModelSettings, UploadedFile
226+
from pydantic_ai import Agent, UploadedFile
227227
from pydantic_ai.models.anthropic import AnthropicModel
228228
from pydantic_ai.providers.anthropic import AnthropicProvider
229229

@@ -236,14 +236,13 @@ async def main():
236236
with open('document.pdf', 'rb') as f:
237237
uploaded_file = await provider.client.beta.files.upload(file=f)
238238

239-
# Reference the uploaded file, including the required beta header
239+
# Reference the uploaded file; the beta header is added automatically
240240
agent = Agent(model)
241241
result = await agent.run(
242242
[
243243
'Summarize this document',
244244
UploadedFile(file_id=uploaded_file.id, provider_name=model.system),
245-
],
246-
model_settings=ModelSettings(extra_headers={'anthropic-beta': 'files-api-2025-04-14'}),
245+
]
247246
)
248247
print(result.output)
249248
#> The document discusses the main topics and key findings...

‎pydantic_ai_slim/pydantic_ai/models/anthropic.py‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255

256256
_ANTHROPIC_SAMPLING_PARAMS = ('temperature', 'top_p', 'top_k')
257257
_ANTHROPIC_TASK_BUDGETS_BETA = 'task-budgets-2026-03-13'
258+
_ANTHROPIC_FILES_API_BETA = 'files-api-2025-04-14'
258259
_ANTHROPIC_COMPACT_EDIT_TYPE = 'compact_20260112'
259260

260261

@@ -760,7 +761,7 @@ async def _messages_create(
760761
)
761762
output_config = self._build_output_config(model_request_parameters, model_settings)
762763
anthropic_profile = self.profile
763-
betas, extra_headers = self._get_betas_and_extra_headers(model_settings, anthropic_profile)
764+
betas, extra_headers = self._get_betas_and_extra_headers(model_settings, anthropic_profile, messages)
764765
betas.update(native_tool_betas)
765766
context_management = self._add_compaction_params(messages, betas, model_settings)
766767
self._validate_task_budget_vs_context_management(model_settings, context_management)
@@ -820,10 +821,12 @@ def _get_betas_and_extra_headers(
820821
self,
821822
model_settings: AnthropicModelSettings,
822823
anthropic_profile: AnthropicModelProfile,
824+
messages: list[ModelMessage],
823825
) -> tuple[set[str], dict[str, str]]:
824826
"""Prepare beta features list and extra headers for API request.
825827
826-
Handles merging custom `anthropic-beta` header from `extra_headers` into betas set
828+
Handles merging custom `anthropic-beta` header from `extra_headers` into betas set,
829+
auto-attaching the Files API beta when messages contain an Anthropic `UploadedFile`,
827830
and ensuring `User-Agent` is set.
828831
"""
829832
extra_headers = dict(model_settings.get('extra_headers', {}))
@@ -845,8 +848,36 @@ def _get_betas_and_extra_headers(
845848
if beta_header := extra_headers.pop('anthropic-beta', None):
846849
betas.update({stripped_beta for beta in beta_header.split(',') if (stripped_beta := beta.strip())})
847850

851+
if self._messages_use_anthropic_uploaded_file(messages):
852+
betas.add(_ANTHROPIC_FILES_API_BETA)
853+
848854
return betas, extra_headers
849855

856+
def _messages_use_anthropic_uploaded_file(self, messages: list[ModelMessage]) -> bool:
857+
"""Whether any normalized message contains an Anthropic-hosted `UploadedFile`.
858+
859+
Used to gate auto-attachment of the `files-api-2025-04-14` beta header.
860+
Mirrors the per-item `provider_name == self.system` check the request
861+
mappers (`_map_user_prompt`, `_map_message`'s `ToolReturnPart` branch)
862+
already perform — so the beta is added exactly when the wire shape
863+
requires it. `UploadedFile`s for other providers are intentionally
864+
ignored here; they will raise the existing `UserError` later in the
865+
request-mapping path.
866+
"""
867+
for message in messages:
868+
if not isinstance(message, ModelRequest):
869+
continue
870+
for part in message.parts:
871+
if isinstance(part, UserPromptPart) and not isinstance(part.content, str):
872+
for item in part.content:
873+
if isinstance(item, UploadedFile) and item.provider_name == self.system:
874+
return True
875+
elif isinstance(part, ToolReturnPart):
876+
for item in part.content_items(mode='raw'):
877+
if isinstance(item, UploadedFile) and item.provider_name == self.system:
878+
return True
879+
return False
880+
850881
def _effective_speed(
851882
self, model_settings: AnthropicModelSettings, anthropic_profile: AnthropicModelProfile
852883
) -> Literal['standard', 'fast'] | Omit:
@@ -932,7 +963,7 @@ async def _messages_count_tokens(
932963
)
933964
output_config = self._build_output_config(model_request_parameters, model_settings)
934965
anthropic_profile = self.profile
935-
betas, extra_headers = self._get_betas_and_extra_headers(model_settings, anthropic_profile)
966+
betas, extra_headers = self._get_betas_and_extra_headers(model_settings, anthropic_profile, messages)
936967
betas.update(native_tool_betas)
937968
context_management = self._add_compaction_params(messages, betas, model_settings)
938969
self._validate_task_budget_vs_context_management(model_settings, context_management)

‎tests/models/cassettes/test_multimodal_tool_returns/test_multimodal_tool_return_matrix[direct-uploaded_file-document-anthropic].yaml‎

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ interactions:
44
accept:
55
- application/json
66
accept-encoding:
7-
- gzip, deflate, br, zstd
7+
- gzip, deflate, br
88
connection:
99
- keep-alive
1010
content-length:
@@ -38,30 +38,33 @@ interactions:
3838
connection:
3939
- keep-alive
4040
content-length:
41-
- '638'
41+
- '664'
4242
content-security-policy:
4343
- default-src 'none'; frame-ancestors 'none'
4444
content-type:
4545
- application/json
4646
strict-transport-security:
4747
- max-age=31536000; includeSubDomains; preload
48+
traceresponse:
49+
- 00-b5b8443acb69f07557dcea0f187fb6de-667748a1cd0cbfba-01
4850
transfer-encoding:
4951
- chunked
5052
vary:
5153
- Accept-Encoding
5254
parsed_body:
5355
content:
54-
- text: I'll retrieve the document file for you.
56+
- text: I'll retrieve the document file for you now.
5557
type: text
5658
- caller:
5759
type: direct
58-
id: toolu_01AxGWRphgS3hyvLovTfpghj
60+
id: toolu_01FThbNViZrZ6SBDCdM9oS8M
5961
input: {}
6062
name: get_file
6163
type: tool_use
62-
id: msg_01WUmhzDAnCuJZiVJYTRz7pc
64+
id: msg_011Ccn6sCPxkTDtNgYHt5sNu
6365
model: claude-sonnet-4-5-20250929
6466
role: assistant
67+
stop_details: null
6568
stop_reason: tool_use
6669
stop_sequence: null
6770
type: message
@@ -73,7 +76,7 @@ interactions:
7376
cache_read_input_tokens: 0
7477
inference_geo: not_available
7578
input_tokens: 558
76-
output_tokens: 46
79+
output_tokens: 47
7780
service_tier: standard
7881
status:
7982
code: 200
@@ -83,11 +86,11 @@ interactions:
8386
accept:
8487
- application/json
8588
accept-encoding:
86-
- gzip, deflate, br, zstd
89+
- gzip, deflate, br
8790
connection:
8891
- keep-alive
8992
content-length:
90-
- '831'
93+
- '835'
9194
content-type:
9295
- application/json
9396
host:
@@ -101,21 +104,21 @@ interactions:
101104
type: text
102105
role: user
103106
- content:
104-
- text: I'll retrieve the document file for you.
107+
- text: I'll retrieve the document file for you now.
105108
type: text
106-
- id: toolu_01AxGWRphgS3hyvLovTfpghj
109+
- id: toolu_01FThbNViZrZ6SBDCdM9oS8M
107110
input: {}
108111
name: get_file
109112
type: tool_use
110113
role: assistant
111114
- content:
112115
- content:
113116
- source:
114-
file_id: file_011CYiV4psfMwLCihcy8Ba6m
117+
file_id: file_011Ccn6h59825RrtpAAPmrBv
115118
type: file
116119
type: document
117120
is_error: false
118-
tool_use_id: toolu_01AxGWRphgS3hyvLovTfpghj
121+
tool_use_id: toolu_01FThbNViZrZ6SBDCdM9oS8M
119122
type: tool_result
120123
role: user
121124
model: claude-sonnet-4-5
@@ -135,25 +138,46 @@ interactions:
135138
connection:
136139
- keep-alive
137140
content-length:
138-
- '293'
141+
- '1168'
139142
content-security-policy:
140143
- default-src 'none'; frame-ancestors 'none'
141144
content-type:
142145
- application/json
143146
strict-transport-security:
144147
- max-age=31536000; includeSubDomains; preload
148+
traceresponse:
149+
- 00-bff9c26e6128504db428c364a7176089-bb063d9e181ebb95-01
145150
transfer-encoding:
146151
- chunked
147152
vary:
148153
- Accept-Encoding
149154
parsed_body:
150-
error:
151-
message: 'messages.2.content.0.tool_result.content.0.document.source: Input tag ''file'' found using ''type'' does
152-
not match any of the expected tags: ''base64'', ''content'', ''text'', ''url'''
153-
type: invalid_request_error
154-
request_id: req_011CYigKmqHtrr2p2vQY7y4v
155-
type: error
155+
content:
156+
- text: "I received a PDF document file. Here's what I can describe about it:\n\n**Document Type:** PDF (application/pdf)\n\n**Content:**
157+
This is a simple, single-page PDF document with minimal content. The page contains only a heading that reads \"Dummy
158+
PDF file\" positioned in the upper left portion of the page. The rest of the page appears to be blank/white space.\n\n**Structure:**
159+
\n- Single page (page 1)\n- Simple text-based content\n- No complex formatting, images, tables, or other elements\n-
160+
Appears to be a test or placeholder PDF file\n\nThis seems to be a sample or test PDF document used for demonstration
161+
purposes rather than a document with substantive content."
162+
type: text
163+
id: msg_011Ccn6sPauiMXjTjsCsSWdr
164+
model: claude-sonnet-4-5-20250929
165+
role: assistant
166+
stop_details: null
167+
stop_reason: end_turn
168+
stop_sequence: null
169+
type: message
170+
usage:
171+
cache_creation:
172+
ephemeral_1h_input_tokens: 0
173+
ephemeral_5m_input_tokens: 0
174+
cache_creation_input_tokens: 0
175+
cache_read_input_tokens: 0
176+
inference_geo: not_available
177+
input_tokens: 2218
178+
output_tokens: 155
179+
service_tier: standard
156180
status:
157-
code: 400
158-
message: Bad Request
181+
code: 200
182+
message: OK
159183
version: 1

0 commit comments

Comments
 (0)