Files, PDFs & images
filePart() and imagePart() — attaching binary media to a message, and how each wire carries a document.
Il contenuto delle pagine di documentazione è in inglese. Navigazione, ricerca e interfaccia seguono la lingua scelta.
The canonical Part union has five members (text, image, tool_use, tool_result, reasoning) and no file or audio kind. 2.0 kept it that way on purpose — a sixth member would break every exhaustive switch, and none of the chat wires offer a native audio part. ImagePart is the carrier for all binary media: a PDF is { type: 'image', image: bytes, mediaType: 'application/pdf' }, and audio you want a chat model to hear travels as filePart({ mediaType: 'audio/…' }). Speech and transcription are dedicated modules, not a sixth part.
That convention is correct but undiscoverable — nobody guesses "attach a PDF as an image". Since 1.9 there are two constructors that turn it into an API, and the convention actually works on all four wires.
filePart()
import { generateText, filePart } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const bytes = new Uint8Array(await file.arrayBuffer());
const { text } = await generateText({
model: anthropic('claude-opus-4-8'),
messages: [
{
role: 'user',
content: [
filePart({ data: bytes, mediaType: 'application/pdf' }),
{ type: 'text', text: 'Summarise this.' },
],
},
],
});function filePart(input: { data: string | Uint8Array; mediaType: string }): ImagePart;
function imagePart(input: { data: string | Uint8Array; mediaType?: string }): ImagePart;Both are exported from the package root (@deuz-sdk/core) and from @deuz-sdk/core/edge.
datamay be raw bytes, a base64 string, adata:URL, or anhttps:URL — passed through on the wires that can fetch one.mediaTypeis required onfilePart: unlike an image there is no sane default, and it is what the adapters classify on.- On
imagePartit is optional — an unlabelled image resolves toimage/jpeg, or is derived from adata:prefix or a URL extension. - Put media first and the question after it. That is the order every provider's own docs recommend.
When the file part kind lands in 2.0 these functions keep their signatures and just return the new part, so callers written today do not change.
How each wire carries a document
A media part whose mediaType is not image/* is mapped to that wire's document block instead of an image block. Before 1.9 it was sent as an image block, which 400d on three of the four wires.
| Surface | Adapter | Document block |
|---|---|---|
anthropic | anthropicAdapter | { type: 'document', source: … } (base64 or url) |
responses | openaiResponsesAdapter | { type: 'input_file', … } (file_url or inline file_data) |
chat_completions | openaiCompatibleAdapter | { type: 'file', file: { filename, file_data } } |
native | googleNativeAdapter | inlineData / fileData |
Ordinary image parts are unchanged, byte for byte, on all four wires.
Documents are gated on the model's capability row
A model that cannot accept a document no longer receives a bogus block: the part is dropped and deps.logger.warn names the model and the media type.
The gate is nativePdf || vision on the resolved capability row. On Anthropic, OpenAI Responses and Chat Completions document ingestion rides the multimodal path, so a vision-capable row can carry one; a text-only slug would 400.
Consequences worth designing for:
- A message may now carry fewer blocks than parts. Check the warning log if a document seems to have been ignored.
- An unknown slug gets the conservative fallback row, which is not vision-capable — so its documents are refused. Fix it with a per-call
capabilitiesoverride. - Chat Completions drops an
https:-URL document: that wire has no URL form on itsfileblock, and the SDK does not fetch bytes on your behalf. Pass bytes or base64 there. - A URL ending
.pdfnow resolves toapplication/pdfinstead of the oldimage/jpegdefault.
Reading the drop
The drop always produces a deps.logger.warn line, and the default logger is a no-op — so wire one if you want to see it at all:
deps: { logger: { debug() {}, info() {}, warn: (m, meta) => console.warn(m, meta), error() {} } }Since 1.9 the same drop can also arrive as a typed CallWarning on streamChat's warnings and as a warning part on fullStream:
const result = streamChat({ model, messages }); // messages carry a filePart()
for (const w of (await result.warnings) ?? []) console.warn(w.type, w.message);
// → 'other' Dropped a 'application/pdf' document: <reason>, so <provider>/<model>
// will answer without it.It is type: 'other' because CallWarning['type'] has no content-specific member and that union is locked.
All three wires that can drop a document thread the warning sink — chat_completions, anthropic and responses all report through one shared helper, so the typed entry lands on result.warnings and (streaming) on fullStream as a warning part. The native Gemini wire accepts PDFs, so it never drops one.
The pre-1.9 deps.logger.warn line is unchanged in shape, and the sink records quietly on top of it: one drop still produces exactly one log line for a log-based workflow.
From a file picker
filesToImageParts (core) and partsFromFiles (React) turn picked files into canonical parts using Web APIs only — await blob.arrayBuffer(), no FileReader, no Buffer — so they behave identically in a browser and on the edge.
import { partsFromFiles } from '@deuz-sdk/react';
<input
type="file"
multiple
onChange={async (e) => {
const parts = await partsFromFiles(e.target.files); // images AND PDFs
await sendMessage({ text: 'what is in these?', parts });
}}
/>;partsFromFiles is the null-tolerant wrapper (it accepts a nullable FileList); filesToImageParts from @deuz-sdk/core/chat is the core function it delegates to. Every blob is fully buffered in memory, so cap the size where the user picks them.
Rendering an attachment back
uiFromMessages used to drop ImageParts, so a restored turn with an attachment rendered as an empty bubble. Since 1.9 each one becomes a file element in the message's ordered parts:
case 'file':
return part.url
? <img key={i} src={part.url} alt="" />
: <Attachment key={i} mediaType={part.mediaType} data={part.data} />;data is the canonical ImagePart.image value verbatim (bytes stay bytes) plus a resolved mediaType. url is set only when the data is already a renderable data: / http(s): src — the reducer will not base64-encode a buffer on a render path. For bytes, build one yourself: URL.createObjectURL(new Blob([part.data], { type: part.mediaType })).
UIMessage.content is unchanged: still text only.
See also
- Messages & parts — the canonical
Partunion. - Prompts, instructions & timeouts — the rest of the 1.9 call-shape additions.
- React hooks —
sendMessage({ text, parts })and ordered-part rendering. - RAG — for extracting text from a document yourself instead of sending it to the model.