Вышел Deuz SDK 2.4 — версионируемая память и компилятор контекста, супервизор целей и надёжный планировщик, локальный Docker-раннер и надёжные потоки чата. Что нового в 2.4
Deuz SDK
Core

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

RoleUse
systemInstructions. 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).
userHuman input — text and media.
assistantModel output you replay back: text, reasoning, and tool_use parts.
toolTool 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:

typeFieldsDirection
texttext: string, providerMetadata?in / out
imageimage: string | Uint8Array, mediaType?: stringin (also carries PDFs/files)
reasoningtext: string, signature?, encrypted?, redacted?, source?out (replay back in)
tool_useid, name, input: unknown, providerMetadata?out (replay back in)
tool_resulttoolUseId: 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 valueDetected asNotes
'https://…' / 'http://…'URLSent as a URL reference where supported; mediaType inferred from the extension.
'data:image/png;base64,…'data URLmediaType parsed from the prefix.
'iVBORw0KGgo…' (bare string)raw base64Pass mediaType (defaults to image/jpeg).
Uint8Arrayraw bytesBase64-encoded for you (edge-safe, no Buffer). Pass mediaType.
vision.ts
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.

WireImage handling
AnthropicURL form → { source: { type: 'url' } }; bytes/base64/data URL → { source: { type: 'base64' } }.
OpenAI / xAI / Gemini-compatAll forms collapse to an image_url (a URL passes through; bytes become a data: URL).
Gemini nativeURL → 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):

pdf-gemini.ts
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 thinking blocks with a signature (or redacted_thinking blocks with opaque data); 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 text is empty but which carries a signature is a real block. Claude 5.x models default to display: '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) and redacted: true (an Anthropic redacted_thinking block) mark opaque payloads. Their text is 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_tokens while 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:

multi-turn.ts
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 system messages are pulled out and joined with blank lines; later ones are placed per wire (2.3).
  • A tool-role message is folded into a user turn on wires (Anthropic, Gemini) that have no dedicated tool role.
  • Unknown future part kinds are additive — keep a default branch when you switch over Part.type.

See also

  • streamChat — streaming responses and fullStream parts.
  • generateText — non-streaming, with response.messages.
  • Tool loop — automatic multi-step tool_use / tool_result handling.

На этой странице