Deuz SDK 2.0 is out — stores, guardrails, handoffs, and zero-config MCP. What is new in 2.0
Deuz SDK

Migrating from the Vercel AI SDK

A verified mapping from ai / @ai-sdk/* to @deuz-sdk/core — plus the parts that have no equivalent.

This page maps the Vercel AI SDK (ai + @ai-sdk/*) onto @deuz-sdk/core. Both are TypeScript AI SDKs with free functions, a canonical delta stream, Zod-typed structured output and a streaming UI protocol, so most of a port is mechanical. The differences that are not mechanical are listed before the tables, and the things Deuz simply does not have are listed at the end — read that section before you commit to a port.

Verification status

Every Deuz name on this page is verified against the source in this repository. Every AI SDK name is verified against the live docs at ai-sdk.dev on 2026-07-28, against ai@7.0.40 (AI SDK 7 was published 2026-06-25). That project renames aggressively across majors — a name in the left column may have moved again by the time you read this. Re-check the AI SDK 7 migration guide before you trust a left-hand cell, and treat this page as a map of concepts, not a compiler. Where a left-hand cell could not be verified it says so; nothing is guessed.

Prefer to port with an agent?

This repo ships a skill for it. Point a coding agent at your app and it will do the mechanical part — imports, route handlers, tool literals, hook call sites — while flagging the cases that need a decision:

npx skills add Deuz-AI/Deuz-SDK   # installs `deuz-sdk` and `migrate-from-ai-sdk`

Then ask: "migrate this app from the AI SDK to @deuz-sdk/core". The skill's rule files cover imports, streaming, tools, UI, telemetry and providers.

Five differences that are not renames

1. No environment variables, ever

The AI SDK's providers read process.env.ANTHROPIC_API_KEY (and friends) for you, and its gateway accepts a plain string model id like 'anthropic/claude-...'. @deuz-sdk/core reads no environment variable and has no hosted gateway: it must run unchanged on Cloudflare Workers, where process.env does not exist. You read the key at your app layer and hand it to a factory.

// AI SDK — key read implicitly
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-sonnet-4-5');
// @deuz-sdk/core — key passed explicitly
import { createAnthropic } from '@deuz-sdk/core/anthropic';
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const model = anthropic('claude-opus-4-8');

Everything ambient goes through one Dependencies seam — fetch, clock, logger, tracer, keyProvider, priceProvider, generateId, observer. There is no console.*, no Date.now() and no Math.random() anywhere in core. See Dependencies.

2. streamChat returns synchronously and never throws

streamText returns immediately too, but Deuz makes it a hard contract (the "G2" rule): streamChat does no I/O at call time, the pump starts lazily on first access of an output, and a failure is never a synchronous throw. It arrives as an error part on fullStream, with rejected usage / finishReason promises. So there is no try/catch around the call — put it around the for await, or await the promise.

The corollary bites in serverless: if nobody reads the stream, the run never reaches its terminal boundary, so onFinish, chat persistence and durable checkpoints never fire. That is what consume() is for — the counterpart of the AI SDK's result.consumeStream().

3. The UI wire is ours, and it is versioned

Deuz does not speak the AI SDK's UI message protocol. toDeuzStreamResponse emits SSE stamped x-deuz-stream: v2, read back by readDeuzStream / connectDeuzStream or by useChat from @deuz-sdk/react. It is additive-versioned (v1 output stays byte-identical) and resumable: every event carries an id: <seq> line, so a client can reconnect with Last-Event-ID and get one gapless sequence. See UI streaming.

You cannot point the AI SDK's useChat at a Deuz route, or Deuz's useChat at an AI SDK route. Both halves of a chat feature move together.

4. An agent is a value, not a class

AI SDK 7's headline abstraction is a class — ToolLoopAgent, plus WorkflowAgent and HarnessAgent. Deuz has no class and no new: the loop is a call option (tools + maxSteps), a reusable agent is createAgent(...) — a frozen plain object of closures over one options template — and an agent-as-a-tool is agentTool() or agent.asTool(), which nests the same loop one level down with approval inheritance and live sub-agent stream forwarding. There is no separate runtime behind any of them: agent.streamChat(o) is streamChat({ ...def, ...o }).

5. Dual ESM + CJS, still

AI SDK 7 is ESM-only — its announcement states that require() is not supported. @deuz-sdk/core still ships both ESM and CJS builds per subpath, with types resolved per condition (checked by publint --strict and attw in CI). Both require Node ≥ 22.

Mapping: core calls

Vercel AI SDK (7.x)@deuz-sdk/coreNotes
streamText(...)streamChat(...)Sync return, never throws, lazy pump.
generateText(...)generateText(...)Awaited buffered call. result.steps present when tools ran.
generateText({ output: Output.object({ schema }) })generateObject({ schema })generateObject / streamObject are absent from the AI SDK 7 Core reference; Output replaced them. Deuz keeps them as separate functions.
streamText({ output: ... })streamObject({ schema })partialObjectStreamDeuz has no Output.array() / elementStream equivalent.
result.stream (fullStream before 7.0)result.fullStreamDeuz's name never changed; the canonical part union is StreamPart.
result.textStreamresult.textStreamSame.
result.consumeStream()result.consume?.()Since 1.9. Optional on the type — two paths do not provide it yet.
result.usage / result.totalUsageresult.usageOne field: totals summed across every step and sub-agent.
result.finishReasonresult.finishReason'stop' | 'length' | 'tool_calls' | 'content_filter' | 'error' | 'aborted'.
result.finalStepresult.steps?.at(-1)Deuz keeps per-step data only in steps.
result.warningsresult.warningsA Promise<CallWarning[]> on the streaming calls (settles with usage, never rejects) and a plain CallWarning[] on generateText / generateObject, with the key omitted when empty. See Partly inert.
instructions (system before 7.0)instructionsSame name, same meaning, since 1.9. Placed first; a system message already in messages is preserved after it.
promptpromptSince 1.9. Shorthand for one user turn, mutually exclusive with messages.
messages (ModelMessage[])messages (Message[])Deuz has no convertToModelMessages step — see the UI table.
abortSignalsignal (abortSignal accepted, deprecated)Both work; signal wins if both are set.
timeouttimeoutBoth accept a number or an object. The AI SDK's object is { totalMs, stepMs, toolMs, tools }; Deuz's is { ttftMs, totalMs, stepMs, toolMs } — no per-tool map (that is Tool.timeoutMs), plus a time-to-first-byte layer the AI SDK has no counterpart for. See Timeouts.
maxRetriesmaxRetriesDeuz retries pre-first-byte only; a mid-stream error is final.
maxOutputTokens, temperature, topP, stopSequencessame namesLocked surface.
embed / embedManyembed / embedManyvalue / values. Auto-batched and concurrency-capped.
the provider's embedding-model accessorcreateOpenAIEmbedding(...), createGoogleEmbedding(...), createVoyage(...)EmbeddingModel is a distinct kind from LanguageModel — never cast between them.
generateImage (experimental_generateImage before 7.0)generateImage (@deuz-sdk/core/image)Plus async Midjourney at @deuz-sdk/core/midjourney.
MockLanguageModelV4 (ai/test)createMockModel (@deuz-sdk/core/testing)Scripted turns; mockFetch / sseResponse / mockFetchSequence for golden-replay fixtures.

Mapping: tools and the agentic loop

Vercel AI SDK (7.x)@deuz-sdk/coreNotes
tool({ inputSchema, execute })tool({ parameters, execute })Deuz kept parameters. Deuz's tool() is a pure identity function — inference only, zero runtime.
stopWhen: isStepCount(n) (stepCountIs before 7.0)maxSteps: n — or stopWhen: stepCountIs(n)Deuz's maxSteps defaults to 1 (single turn). This is the single most common porting bug.
stopWhen (array, OR-ed)stopWhen (single or array, OR-ed with maxSteps)Deuz also ships hasToolCall, totalTokensExceed, costExceeds, durationExceeds.
activeTools (experimental_activeTools before 7.0)activeToolsSame meaning. Matching nothing fails open in Deuz (the full set is sent).
prepareStep (experimental_prepareStep before 7.0)prepareStepReturns { messages?, activeTools?, toolChoice?, model? }. Runs after automatic compaction.
onStepEnd (onStepFinish before 7.0)onStepFinishDeuz kept the older name.
onEnd (onFinish before 7.0)onFinishDeuz kept the older name. onUsage is separate.
toolChoicetoolChoice'auto' | 'required' | 'none' | { type: 'tool', toolName }.
toolApproval (needsApproval before 7.0)needsApproval (per tool) + approveToolCall / approvalResponsesDeuz kept needsApproval on the tool. Server mode decides inline; client mode breaks the loop into pendingApprovals / tool-approval-request parts. HMAC-signed tokens via createApprovalSigner.
toolCall.input / toolResult.outputToolCall.args / ToolResult.resultDeuz's canonical parts are tool_use (input) and tool_result (result). The UI wire uses input / output.
ToolExecutionOptions (ToolCallOptions before 7.0)ToolExecuteContext{ toolCallId, messages, signal? } plus agentPath inside a sub-agent.
contextSchema / toolsContext / runtimeContextNo equivalent. Close over what a tool needs, or read it from ctx.messages.
ToolLoopAgentcreateAgent({ ... })A frozen value, not a class. agent.asTool() for nesting.
WorkflowAgent (durable)session: { store, runId } + resumeFromCheckpointDurable runtime — checkpoints go in your store; there is no workflow vendor.
provider-executed toolsanthropicWebSearch(), openaiWebSearch(), googleSearch()Provider-executed tools. Passing one to a chat_completions-surface model drops it and logs a warning.
createMCPClientcreateMcpClient (@deuz-sdk/core/mcp, /mcp/stdio)@modelcontextprotocol/sdk is a lazy optional peer. listTools() returns a canonical ToolSet.

Mapping: UI, React and the wire

Vercel AI SDK (7.x)@deuz-sdk/coreNotes
result.toUIMessageStreamResponse()toDeuzStreamResponse(result) (@deuz-sdk/core/ui)Different protocol, not a drop-in.
result.toTextStreamResponse()toDeuzTextStreamResponse(result)Since 1.9. text/plain, no framing. A mid-stream failure truncates the body — it never injects error text.
createUIMessageStream / createUIMessageStreamResponse + a data-* writecreateDeuzStream(result)writeData(name, payload, { id?, transient? })Since 1.9 a data part can be addressable (id, reconciled client-side) or transient (on the wire, off the journal).
useChat (@ai-sdk/react)useChat (@deuz-sdk/react)Both hooks, very different surfaces — see below.
transport: new DefaultChatTransport({ api })useChat({ api })Deuz has no transport abstraction; fetch is injectable.
messages (option) / setMessagesinitialMessages (read once at mount) / setMessages / setHistoryDeuz keeps two views. setMessages re-derives canonical via the lossy canonicalFromUI; setHistory({ ui, canonical }) replaces both.
UIMessage.partsUIMessage.parts (UIMessagePart[])Since 1.9, and optional — absent until the first element exists. Different member set; see rendering ordered parts.
addToolResultaddToolResult({ toolCallId, output, isError? })Since 1.9. Answers a parked client tool call.
resumeStreamresume: { endpoint, auto?, cursor? } + reconnect()auto fires once per mounted hook and fails silently by design.
regenerate / stop / clearErrorregenerate / stop / clearErrorSame names. Deuz also has editAndResend(messageId, input).
onToolCall / onData / onErrorsame namesDeuz's onData receives the raw frame, before reconciliation.
convertToModelMessages(messages)canonicalFromUI(ui) — or don'tDeuz's useChat already holds the canonical history and POSTs it, so a route normally reads Message[] directly. canonicalFromUI is the lossy inverse for apps that own the UI array.
— (no equivalent)validateChatRequest(body)Deuz's route receives a canonical Message[], which includes role: 'system' — so it ships a structural validator. Use it.
useObjectuseObjectReads object-delta parts from toDeuzObjectStreamResponse.
useCompletionNot implemented. Use useChat against a single-turn route.
Svelte / Vue / Angular bindingsNot implemented. The wire is framework-agnostic; only React ships.

Mapping: providers

Deuz's provider modules are dedicated subpaths of the one package, not separate packages — there is nothing to add to package.json per provider.

Vercel AI SDK@deuz-sdk/core
@ai-sdk/anthropiccreateAnthropic@deuz-sdk/core/anthropiccreateAnthropic, anthropic
@ai-sdk/openaicreateOpenAI@deuz-sdk/core/openaicreateOpenAI (Chat Completions), createOpenAIResponses (Responses API), createOpenAIEmbedding
@ai-sdk/google@deuz-sdk/core/googlecreateGoogle (compat) / createGoogleNative (full wire: reasoning, cache, native PDF)
@ai-sdk/google-vertex@deuz-sdk/core/vertexcreateVertexAnthropic, createVertexGoogle, createVertexGoogleNative
@ai-sdk/xai@deuz-sdk/core/xaicreateXai
@ai-sdk/azure@deuz-sdk/core/azurecreateAzure
@ai-sdk/amazon-bedrock@deuz-sdk/core/bedrockcreateBedrock
@ai-sdk/openai-compatiblecreateOpenAICompatible@deuz-sdk/core/providerscreateOpenAICompatible({ id, baseURL })
@ai-sdk/groq, @ai-sdk/mistral, @ai-sdk/deepseek, …@deuz-sdk/core/providerscreateGroq, createMistral, createDeepSeek, createTogether, createOpenRouter, createCerebras, createFireworks, createMoonshot / createKimi, createQwen, createGLM, createMiniMax
customProvider (experimental_customProvider before 7.0) / gateway string idscreateProviderRegistry({ ... })registry.model('groq:llama-4-maverick')
wrapLanguageModel + middlewarewrapModel(model, [...]) (@deuz-sdk/core/middleware)

Canonical part names

Both SDKs normalize provider SSE into a delta union first, but the strings differ. Deuz's fullStream yields StreamPart:

AI SDK part typeDeuz StreamPart type
text-deltatext-delta (payload field: text)
reasoning-deltareasoning-delta (payload field: text)
tool-input-deltatool-call-delta ({ id, name?, argsTextDelta })
tool-calltool-call ({ toolCallId, toolName, input })
tool-resulttool-result ({ toolCallId, toolName, output, isError? })
start-step / finish-stepstep-start / step-finish
sourcesource
finishfinish ({ usage, finishReason, providerMetadata? })
errorerror ({ error: unknown } — the UI wire reframes this to { message: string })

Deuz adds members the AI SDK has no counterpart for: compaction, sub-agent, citation, cost, budget-exceeded, verify, plan-update, activity, tool-state, tool-approval-request. The union is documented as open — keep a default case.

Code: basic stream

before-stream.ts
// Vercel AI SDK
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

const result = streamText({
  model: anthropic('claude-sonnet-4-5'),
  instructions: 'You are terse.',
  prompt: 'Write a haiku about TypeScript.',
});

for await (const chunk of result.textStream) process.stdout.write(chunk);
after-stream.ts
// @deuz-sdk/core
import { streamChat } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const result = streamChat({
  model: anthropic('claude-opus-4-8'),
  instructions: 'You are terse.',
  prompt: 'Write a haiku about TypeScript.',
});

try {
  for await (const chunk of result.textStream) process.stdout.write(chunk);
} catch {
  // Mid-stream failure. The `streamChat(...)` call itself never throws.
}
const usage = await result.usage;

Code: tools

Deuz kept parameters; the AI SDK calls it inputSchema. Everything else lines up.

before-tools.ts
// Vercel AI SDK
import { generateText, tool, isStepCount } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';

const { text } = await generateText({
  model: anthropic('claude-sonnet-4-5'),
  prompt: 'Weather in Paris?',
  stopWhen: isStepCount(5),
  tools: {
    getWeather: tool({
      description: 'Get the weather for a city',
      inputSchema: z.object({ city: z.string() }),
      execute: async ({ city }) => ({ city, tempC: 22 }),
    }),
  },
});
after-tools.ts
// @deuz-sdk/core
import { generateText, tool } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { z } from 'zod';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const { text, steps } = await generateText({
  model: anthropic('claude-opus-4-8'),
  prompt: 'Weather in Paris?',
  maxSteps: 5, // REQUIRED — the default is 1, i.e. a single turn
  tools: {
    getWeather: tool({
      description: 'Get the weather for a city',
      parameters: z.object({ city: z.string() }),
      execute: async (args) => ({ city: args.city, tempC: 22 }), // args: { city: string }
    }),
  },
});

tool() is optional — a plain object literal still works, it just types args as unknown. Parallel execution (maxToolConcurrency, default 5), self-healing on a thrown tool, immutable cache-safe history and runaway guards are built in. See Tools and the tool loop.

Code: a reusable agent

before-agent.ts
// Vercel AI SDK
import { ToolLoopAgent } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

const support = new ToolLoopAgent({
  model: anthropic('claude-sonnet-4-5'),
  instructions: 'You are a terse support agent.',
  tools: { lookupOrder },
});

const { text } = await support.generate({ prompt: 'where is order 12?' });
after-agent.ts
// @deuz-sdk/core — a frozen value, no `new`
import { createAgent } from '@deuz-sdk/core/agent';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const support = createAgent({
  name: 'support',
  model: anthropic('claude-opus-4-8'),
  instructions: 'You are a terse support agent.',
  tools: { lookupOrder },
  maxSteps: 8,
});

const { text } = await support.generateText({ prompt: 'where is order 12?' });
const res = support.streamChat({ prompt: 'where is order 12?' }); // sync (G2)

The method names are the free functions' names — generateText, streamChat, generateObject, streamObject — because that is exactly what they forward to. See createAgent for the merge rule and its one sharp edge.

Code: structured output

generateObject / streamObject are absent from AI SDK 7's Core reference; Deuz keeps them as their own functions with their own strategy picker.

before-object.ts
// Vercel AI SDK
import { generateText, Output } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';

const { output } = await generateText({
  model: anthropic('claude-sonnet-4-5'),
  prompt: 'Capital of France as JSON.',
  output: Output.object({ schema: z.object({ city: z.string() }) }),
});
after-object.ts
// @deuz-sdk/core
import { generateObject } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { z } from 'zod';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const { object } = await generateObject({
  model: anthropic('claude-opus-4-8'),
  prompt: 'Capital of France as JSON.',
  schema: z.object({ city: z.string() }),
  // mode: 'auto' — 'json' | 'tool' to force a strategy
});

mode: 'auto' picks the JSON strategy when the model's registry row reports native structured output, else tool-call coercion. On a parse/validation miss Deuz runs one repair retry, then throws NoObjectGeneratedError. streamObject has no repair retry (partials cannot be un-streamed).

Structured output in Deuz is single-turn: since 1.9, passing loop options (tools, maxSteps > 1, stopWhen, verifyStep, memory, …) to generateObject / streamObject raises an InvalidRequestError before any network call instead of silently ignoring them. If you are porting an Output.object call that also passed tools, that is the one place the port will fail loudly — which is the point.

Code: chat route

before-route.ts
// Vercel AI SDK — app/api/chat/route.ts
import { streamText, convertToModelMessages, type UIMessage } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();
  const result = streamText({
    model: anthropic('claude-sonnet-4-5'),
    messages: convertToModelMessages(messages),
  });
  return result.toUIMessageStreamResponse();
}
after-route.ts
// @deuz-sdk/core — app/api/chat/route.ts
import { streamChat } from '@deuz-sdk/core';
import { validateChatRequest } from '@deuz-sdk/core/chat';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { toDeuzStreamResponse } from '@deuz-sdk/core/ui';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

export async function POST(req: Request): Promise<Response> {
  // The body is attacker-controlled and canonical Message[] includes role:'system'.
  const parsed = validateChatRequest(await req.json());
  if (!parsed.ok) return Response.json({ issues: parsed.issues }, { status: 400 });

  const result = streamChat({
    model: anthropic('claude-opus-4-8'),
    instructions: 'You are a helpful assistant.',
    messages: parsed.request.messages,
    signal: req.signal,
  });
  return toDeuzStreamResponse(result);
}

There is no convertToModelMessages step: useChat from @deuz-sdk/react already keeps the canonical Message[] alongside the render view and POSTs that. If your route serves client tools (a useChat with onToolCall), the browser POSTs a role: 'tool' message, so pass validateChatRequest(body, { rejectToolResults: false }) — and read what that accepts.

Code: reading the stream without React

client.ts
import { readDeuzStream } from '@deuz-sdk/core/ui';

const res = await fetch('/api/chat', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ messages }),
});

for await (const part of readDeuzStream(res)) {
  switch (part.type) {
    case 'text-delta':
      appendText(part.text);
      break;
    case 'tool-call':
      showToolCall(part.toolName, part.input);
      break;
    case 'finish':
      done(part.finishReason, part.usage);
      break;
    case 'error':
      showError(part.message); // already secret-redacted
      break;
    default:
      break; // open union — keep a default case
  }
}

Since 1.9 a non-2xx response yields exactly one error part instead of ending silently, so a failed route no longer renders as an empty assistant bubble. Opt out with readDeuzStream(res, { onHttpError: 'ignore' }).

Not implemented, or deliberately different

The previous version of this page ended with "everything else has a direct equivalent above". That was false. Here is the real list.

Not implemented — no equivalent today

AI SDK featureStatus in Deuz
transcribe, generateSpeech (audio in/out)Absent. No transcription or speech function exists. Some models report an audio capability in the registry (audio tokens are metered), but there is no audio entry point.
Video generation (experimental_generateVideo)Absent as a function. @deuz-sdk/core/yunwu carries a pinned YUNWU_VIDEO_MODELS catalog only.
useCompletionAbsent. Use useChat against a single-turn route, or drive readDeuzStream yourself.
Svelte / Vue / Angular bindingsAbsent. Only @deuz-sdk/react ships. The wire is plain SSE, so a binding is writable in an afternoon.
DevTools (@ai-sdk/devtools, a local web UI on a port)No server, but there is a viewer. renderRunReport(events) (@deuz-sdk/core/observe) turns the same ObserveEvent protocol into one self-contained HTML document — inline CSS/JS, no external fetch, opens from file:// — and writeRunReport({ from: 'runs.jsonl', to: 'run.html' }) (@deuz-sdk/core/observe/node) does it straight from a createJsonlObserver file. Nothing listens on a port and nothing records by default.
OpenTelemetry integration (@ai-sdk/otel, registerTelemetry())@deuz-sdk/core/otelcreateOtelTracer() / createOtelObserver(), with @opentelemetry/api as an OPTIONAL peer resolved lazily (no dependency, no bundled shim). Span names and gen_ai.* attributes follow the GenAI semantic conventions, so prebuilt dashboards work. Two differences on purpose: there is no global registration — you pass it through createClient({ deps: { tracer } }), so nothing is ambient and tests stay deterministic — and content capture is opt-in (captureContent) and always double-redacted, rather than on by default. See Observability.
Output.array() / element streamingAbsent. streamObject streams growing partials of one object; there is no per-element stream.
contextSchema / toolsContext / runtimeContextAbsent. A tool closes over what it needs; ToolExecuteContext carries toolCallId, messages, signal and (inside a sub-agent) agentPath.
Codemods (@ai-sdk/codemod)Absent. Deuz ships the migrate-from-ai-sdk agent skill instead.
Hosted model gateway / plain string model idsDeliberately absent. createProviderRegistry is a local descriptor lookup with zero network.

Deliberately different

  • maxSteps defaults to 1. A tool-using call that forgets maxSteps gets one turn, finishReason: 'tool_calls' and no answer. Port every loop with an explicit bound.
  • parameters, not inputSchema. Deuz's Tool shape predates the rename and is part of a locked 1.0 surface.
  • Retry is pre-first-byte only. Once bytes stream, a mid-stream error is final — there is no mid-stream resume of a provider call. (Resuming the UI stream is a separate, supported thing: wire v2 + StreamStateStore.)
  • A canonical Part union with five members (text, image, tool_use, tool_result, reasoning) and no file kind until 2.0. A PDF is an image part with mediaType: 'application/pdf'; use filePart() so you never have to know that.
  • No system option name. Deuz calls it instructions (as AI SDK 7 now does too) and a system-role message in messages is preserved rather than rejected.
  • Approvals are a loop primitive, not a UI state. A gated call either resolves inline (approveToolCall) or breaks the loop; needsApproval stays on the tool.

Partly inert — check before you port onto these

warnings covers the same ground as the AI SDK's, but it is shaped differently and one notice still does not reach it.

The AI SDK's result.warnings reports dropped settings on every result. Deuz's does too — as a promise on the streaming calls, as a plain array on the buffered ones:

const result = streamChat({ model, prompt, temperature: 0.7 });
for (const w of (await result.warnings) ?? []) console.warn(w.type, w.setting, w.message);

What does not carry over:

  • On the buffered calls the field is omitted, not empty, when there is nothing to report. generateText / generateObject leave warnings undefined on a clean call rather than [], so read (result.warnings ?? []). The streaming calls always resolve an array.
  • The buffered loop drops its own activeTools notice. A generateText whose activeTools names no real tool logs the mismatch and carries on, but the notice never reaches result.warnings; the streaming loop records it.
  • useObject cannot receive one. The object wire carries no canonical part stream, so a warning never crosses it. Read warnings server-side on a streamObject result.
  • clamped-setting has no producer. That member of the union ships unused; the unknown-slug maxOutput clamp happens per adapter, not centrally.

So during a port, wire a logger regardless:

deps: {
  logger: {
    debug() {}, info() {},
    warn: (msg, meta) => console.warn('[deuz]', msg, meta),
    error: (msg, meta) => console.error('[deuz]', msg, meta),
  },
}

The full residual list is on Declared but inert.

Two things that no longer need a workaround

Both were plumbing-only when 1.9 first landed and now have producers, so a port should use them directly:

  • Tool approval denial. The streaming loop sets denied / deniedReason on its tool-state parts, so UIToolCall.denied is real: a refused call renders as declined instead of "getWeather failed", and a tool that threw gains no denial fields. The AI SDK has no equivalent. See rendering a denial.
  • sub-agent parts in useChat. applyUIPart folds each frame into turn.subAgents (one per agentPath, carrying the child's own full AssistantTurnState) and useChat exposes subAgents. You no longer need to read the wire by hand with readDeuzStream to render a nested agent. See rendering a sub-agent run.

What Deuz has that the AI SDK does not

Not a competition — just the parts of a port where you get to delete code:

  • Durable runs without a workflow vendor. Step checkpoints in your own store, resumeFromCheckpoint, and a resumable UI wire, so a refresh, a network blip and a server crash all look the same to the client. See the unbreakable chatbot.
  • Automatic layered compaction (compaction: 'auto') — prune tool results, prune reasoning, summarize — inside the loop, prefix-stable so prompt-cache hits survive.
  • Budget guardrailsbudget: { usd, tokens }, costExceeds, totalTokensExceed, all reading real provider-reported usage, with a live cost part on the wire.
  • verifyStep — a verifier hook at every natural completion that can reject an answer and re-drive the loop with feedback.
  • Memory and RAG in the box — mem0-style extract/reconcile over a vector store or a markdown vault; hybrid dense + BM25 retrieval with RRF fusion.
  • A structural request validator (validateChatRequest) for the exact body a chat route receives.
  • Zero runtime dependencies, an edge-safety lint gate, and injected clock/randomness — so tests are deterministic and the same build runs on Workers.

See also

On this page