Messages & Parts
The canonical Message/Part model every provider speaks — roles, text, vision, PDFs, and reasoning round-trips.
Текст страниц документации на английском. Навигация, поиск и интерфейс следуют выбранному языку.
Every call into @deuz-sdk/core takes a Message[]. This is the SDK's single canonical conversation format: you build messages once, and each adapter serializes them to its own wire (Anthropic blocks, OpenAI content arrays, Gemini parts). You never hand-roll a provider's request shape.
Messages
A Message has a role and content. content is either a plain string (shorthand for a single text part) or an array of Parts.
import type { Message } from '@deuz-sdk/core';
const messages: Message[] = [
{ role: 'system', content: 'You are concise.' },
{ role: 'user', content: 'Hello' }, // string shorthand
{
role: 'user',
content: [{ type: 'text', text: 'Hello' }], // explicit Part[]
},
];Internally, string content is coerced to a single TextPart and author order is preserved. The two forms above are equivalent.
A Message may also carry an optional providerMetadata?: Record<string, unknown> — message-level provider round-trip data (e.g. OpenAI Responses' { openai: { phase } }). It is additive and normally set and round-tripped by the SDK (it appears on the assistant messages in response.messages), not something you hand-author.
Roles
| Role | Use |
|---|---|
system | Instructions. The leading run of system messages is joined (blank line) into the provider's top-level system slot; a system message later in the history is a mid-conversation system message, placed per wire (2.3). |
user | Human input — text and media. |
assistant | Model output you replay back: text, reasoning, and tool_use parts. |
tool | Tool execution results (tool_result parts). The agentic loop emits these for you. |
System handling is automatic: adapters with a top-level system field take the leading system messages; a later one stays in place on Claude 5.x, OpenAI Responses and Chat Completions, and is folded into the top-level instruction on Gemini and older Claude models (Preserved thinking). You just add system messages anywhere in the array.
Parts
Part is a discriminated union on type. The full set is locked in the 1.0 surface:
type | Fields | Direction |
|---|---|---|
text | text: string, providerMetadata? | in / out |
image | image: string | Uint8Array, mediaType?: string | in (also carries PDFs/files) |
reasoning | text: string, signature?, encrypted?, redacted?, source? | out (replay back in) |
tool_use | id, name, input: unknown, providerMetadata? | out (replay back in) |
tool_result | toolUseId: string, result: unknown, isError? | in |
There is no separate file part — non-image binary input (PDF, audio, video) rides on the image part via its mediaType. Since 1.9 there are constructors for it, so you never have to know that: filePart() / imagePart(). See also PDFs and other files.
import type {
Part,
TextPart,
ImagePart,
ReasoningPart,
ToolUsePart,
ToolResultPart,
} from '@deuz-sdk/core';Vision inputs
An ImagePart's image field accepts four source forms. The SDK resolves each into the right wire shape per provider:
image value | Detected as | Notes |
|---|---|---|
'https://…' / 'http://…' | URL | Sent as a URL reference where supported; mediaType inferred from the extension. |
'data:image/png;base64,…' | data URL | mediaType parsed from the prefix. |
'iVBORw0KGgo…' (bare string) | raw base64 | Pass mediaType (defaults to image/jpeg). |
Uint8Array | raw bytes | Base64-encoded for you (edge-safe, no Buffer). Pass mediaType. |
import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { readFile } from 'node:fs/promises';
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const bytes = new Uint8Array(await readFile('./chart.png'));
const { text } = await generateText({
model: anthropic('claude-opus-4-8'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'What does this chart show?' },
{ type: 'image', image: bytes, mediaType: 'image/png' },
],
},
],
});A URL or data URL works the same way:
const message: Message = {
role: 'user',
content: [
{ type: 'text', text: 'Describe this image.' },
{ type: 'image', image: 'https://example.com/cat.jpg' },
],
};Provider support differences
Vision availability comes from the capability registry (vision flag), not from the part itself. The part is portable; only the model decides whether it can read the pixels.
| Wire | Image handling |
|---|---|
| Anthropic | URL form → { source: { type: 'url' } }; bytes/base64/data URL → { source: { type: 'base64' } }. |
| OpenAI / xAI / Gemini-compat | All forms collapse to an image_url (a URL passes through; bytes become a data: URL). |
| Gemini native | URL → fileData.fileUri; everything else → inlineData. |
Older text-only models (e.g. some GPT/Grok base chat models) have vision: false in the registry; sending an image to them is a usage error, not an SDK feature. Check capabilities if you support arbitrary model ids.
PDFs and other files
Deliver a document as an image part with a non-image/* mediaType — or, more discoverably, build it with filePart({ data, mediaType }).
Documents work on all four wires since 1.9
Before 1.9 a non-image media part was sent as an image block, which 400d on three of the four wires — so this section was Gemini-native advice. Each adapter now maps it to that wire's document block: Anthropic document, OpenAI Responses input_file, Chat Completions file, Gemini inlineData / fileData.
Documents are gated on the model's capability row (nativePdf || vision); a model that fails the gate has the part dropped with a deps.logger.warn naming the model and media type. Chat Completions also drops an https:-URL document — that wire has no URL form on its file block. See Files, PDFs & images.
Gemini native is still the richest path (nativePdf: true for gemini-2.5-flash, gemini-2.5-pro, gemini-3-pro, gemini-3.5-flash):
import { generateText } from '@deuz-sdk/core';
import { createGoogleNative } from '@deuz-sdk/core/google';
import { readFile } from 'node:fs/promises';
const google = createGoogleNative({ apiKey: process.env.GEMINI_API_KEY! });
const pdf = new Uint8Array(await readFile('./report.pdf'));
const { text } = await generateText({
model: google('gemini-2.5-flash'),
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'Summarize the key findings.' },
{ type: 'image', image: pdf, mediaType: 'application/pdf' },
],
},
],
});For media too large to inline (roughly >20 MB), upload it through the Files API and reference the returned uri instead of raw bytes:
import { uploadFile } from '@deuz-sdk/core/google/extras';
const file = await uploadFile({
apiKey: process.env.GEMINI_API_KEY!,
bytes: pdf,
mimeType: 'application/pdf',
});
// Then use { type: 'image', image: file.uri, mediaType: 'application/pdf' }.The uri is an https:// URL, so it resolves to the native fileData form automatically. See Gemini extras for uploadFile, waitForFileActive, and explicit context caching.
Reasoning parts
ReasoningPart carries a model's thinking output. It is produced on the way out (as reasoning-delta events on fullStream, and as reasoning parts in response.messages) and must be replayed back in multi-step tool loops. Dropping it breaks the next turn:
- Anthropic emits
thinkingblocks with asignature(orredacted_thinkingblocks with opaquedata); each must be returned unmodified and in its original place. - Gemini carries a
thoughtSignature; without it the follow-up request 400s. - OpenAI Responses uses encrypted reasoning items.
- DeepSeek and Kimi stream
reasoning_content, which rides back on the assistant's tool-call turn.
The agentic loop in generateText and streamChat preserves these automatically — each step builds a new history array that includes the prior reasoning and tool_use parts, so you rarely construct a ReasoningPart by hand. The shape:
import type { ReasoningPart } from '@deuz-sdk/core';
const reasoning: ReasoningPart = {
type: 'reasoning',
text: 'The user wants a summary…',
signature: 'opaque-provider-signature',
source: 'anthropic',
};One part per block (2.3)
Every block the provider sends is its own reasoning part, kept where it arrived in the turn:
- A thinking block whose
textis empty but which carries asignatureis a real block. Claude 5.x models default todisplay: 'omitted', which streams only the signature, and that block must ride back like any other. - Two consecutive thinking blocks stay two parts, each with its own signature. A merged block is a modified one, which Anthropic rejects.
encrypted: true(an OpenAI item) andredacted: true(an Anthropicredacted_thinkingblock) mark opaque payloads. Theirtextis not display text; never render it.- Blocks are never reordered: thinking that sits between two tool calls stays between them.
- A turn that produced neither text nor a tool call (cut off by
max_tokenswhile thinking, for example) is not stored at all: its reasoning alone is no answer, and no provider takes it back.
On the stream, each reasoning-delta carries the block's id (deltas of one block share it), redacted when its text is opaque data, and its source. The display text of a step (StepResult.reasoningText) joins the readable blocks only.
Reasoning stays with its provider
source records the provider family whose API produced the block: 'anthropic', 'openai', 'google', or the Chat Completions host's provider id (for example 'deepseek'). Each wire replays only its own family's reasoning, so a conversation that moves to another provider (a fallback, a handoff, a prepareStep model switch, or your own choice) never sends it an Anthropic signature, an OpenAI encrypted item or a Gemini thought signature it cannot read. The blocks stay in the history for the provider that can use them.
Between Claude models the blocks are forwarded unchanged; the API drops the ones the new model cannot read. A Claude thinking block without a signature is never sent (the API always rejects it).
A part written before 2.3 has no source. It is replayed as in 2.2, except encrypted parts (only OpenAI Responses produced them) and redacted parts (only Anthropic did), whose origin is known.
thoughtSignature round-trip on Gemini
On Gemini native, a streamed tool call arrives with its thoughtSignature attached. The adapter stores it on the ToolUsePart's providerMetadata so the next turn echoes it back verbatim:
// A tool_use part returned by Gemini native:
{
type: 'tool_use',
id: 'call_1',
name: 'get_weather',
input: { city: 'Paris' },
providerMetadata: { google: { thoughtSignature: 'sig-123' } },
}Since 2.3 a plain text part keeps its signature too, often streamed in a final chunk whose text is empty: it lands on the text it belongs to as providerMetadata: { google: { thoughtSignature } } and rides back on that text part. Gemini does not enforce text signatures, but omitting them degrades its reasoning (not live-verified).
When you replay these parts, the adapter re-attaches thoughtSignature to the outgoing functionCall or text part. Preserve providerMetadata untouched — it is opaque round-trip data, not something to inspect or rewrite.
Multi-turn with tools
A complete conversation history mixes all part kinds. Roles map directly to provider turns; the assistant turn holds tool_use, and the tool turn holds the matching tool_result keyed by toolUseId:
import type { Message } from '@deuz-sdk/core';
const history: Message[] = [
{ role: 'user', content: 'What is the weather in Paris?' },
{
role: 'assistant',
content: [
{ type: 'text', text: 'Let me check.' },
{ type: 'tool_use', id: 'call_1', name: 'get_weather', input: { city: 'Paris' } },
],
},
{
role: 'tool',
content: [
{ type: 'tool_result', toolUseId: 'call_1', result: { tempC: 18, sky: 'clear' } },
],
},
];Every tool_use must be answered by a tool_result with a matching toolUseId, or Anthropic returns a 400. A failed tool is reported with isError: true rather than omitting the result. When you let the SDK run the tool loop, it enforces this for you and appends the assistant/tool turns to response.messages.
Normalization rules
- String content → a single
TextPart. Author order is never reordered: since 2.3 not even reasoning, which keeps its place on every wire. - The leading
systemmessages are pulled out and joined with blank lines; later ones are placed per wire (2.3). - A
tool-role message is folded into auserturn on wires (Anthropic, Gemini) that have no dedicated tool role. - Unknown future part kinds are additive — keep a
defaultbranch when you switch overPart.type.
See also
- streamChat — streaming responses and
fullStreamparts. - generateText — non-streaming, with
response.messages. - Tool loop — automatic multi-step
tool_use/tool_resulthandling.