Deuz SDK 2.0 ya está aquí — stores, guardrails, handoffs y MCP cero-config. Novedades de 2.0
Deuz SDK
Core

Files, PDFs & images

filePart() and imagePart() — attaching binary media to a message, and how each wire carries a document.

El contenido de las páginas de documentación está en inglés. La navegación, la búsqueda y la interfaz siguen el idioma elegido.

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()

pdf.ts
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.

  • data may be raw bytes, a base64 string, a data: URL, or an https: URL — passed through on the wires that can fetch one.
  • mediaType is required on filePart: unlike an image there is no sane default, and it is what the adapters classify on.
  • On imagePart it is optional — an unlabelled image resolves to image/jpeg, or is derived from a data: 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.

SurfaceAdapterDocument block
anthropicanthropicAdapter{ type: 'document', source: … } (base64 or url)
responsesopenaiResponsesAdapter{ type: 'input_file', … } (file_url or inline file_data)
chat_completionsopenaiCompatibleAdapter{ type: 'file', file: { filename, file_data } }
nativegoogleNativeAdapterinlineData / 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 capabilities override.
  • Chat Completions drops an https:-URL document: that wire has no URL form on its file block, and the SDK does not fetch bytes on your behalf. Pass bytes or base64 there.
  • A URL ending .pdf now resolves to application/pdf instead of the old image/jpeg default.

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 onlyawait blob.arrayBuffer(), no FileReader, no Buffer — so they behave identically in a browser and on the edge.

upload.tsx
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

En esta página