Migrating from Vercel AI SDK
Map ai / @ai-sdk/* calls to @deuz-sdk/core — streamChat, generateText, generateObject, tools, and the UI wire.
If you know the Vercel AI SDK (ai + @ai-sdk/*), most of @deuz-sdk/core will feel familiar: free functions like generateText / generateObject, a Zod-typed structured-output call, and a streaming UI wire. The shapes are close enough that a port is mostly mechanical. The big differences are philosophical — @deuz-sdk/core reads no environment variables (you pass apiKey explicitly), has zero runtime dependencies, and streamChat returns synchronously and never throws. This page maps the API surface and flags the few remaining gaps.
API mapping
| Vercel AI SDK | @deuz-sdk/core | Notes |
|---|---|---|
streamText(...) | streamChat(...) | Returns synchronously, never throws. Pump starts lazily. |
generateText(...) | generateText(...) | await-ed buffered call. result.steps is present when tools run. |
generateObject(...) | generateObject(...) | schema accepts a Zod schema (via Standard Schema) or a raw JSON Schema. |
embed / embedMany | embed / embedMany | value / values; auto batched + concurrency-capped. |
tool({ ... }) helper | plain object with parameters | No wrapper function — see Tools. |
createAnthropic (@ai-sdk/anthropic) | createAnthropic (@deuz-sdk/core/anthropic) | Factory returns a Provider; call it with a model id for a descriptor. |
createOpenAI, openai.embedding(...) | createOpenAI, openaiEmbedding (@deuz-sdk/core/openai) | Embeddings are a separate factory. |
toDataStreamResponse / toUIMessageStreamResponse | toDeuzStreamResponse (@deuz-sdk/core/ui) | Our own versioned wire, not the AI SDK protocol. |
useChat (React hook) | useChat (@deuz-sdk/core/react) | Client tools auto-round-trip via onToolCall; approvals pause into pendingApprovals. React is an optional peer. |
useObject (React hook) | useObject (@deuz-sdk/core/react) | Streams object-delta parts from toDeuzObjectStreamResponse. |
streamObject | streamObject | Same options as generateObject; partialObjectStream yields DeepPartial<T>. No repair retry (partials can't be un-streamed). |
needsApproval / tool approval | needsApproval + approveToolCall / approvalResponses | Server mode decides inline; client mode breaks the loop with tool-approval-request parts and resumes via approvalResponses. |
Conceptual differences
No env auto-read — inject the key
The AI SDK auto-reads process.env.ANTHROPIC_API_KEY (and friends) inside the provider. @deuz-sdk/core is a pure core: it never touches process.env, Date.now(), Math.random(), or console. Read the key at your app layer and pass it into the factory.
// Vercel AI SDK — key read implicitly from process.env
import { anthropic } from '@ai-sdk/anthropic';
const model = anthropic('claude-3-5-sonnet');// @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 stateful or non-deterministic (fetch, clock, logger, id generation, API keys, metering) flows through one Dependencies seam. You can also resolve keys per call with deps.keyProvider. See Dependencies.
streamChat is synchronous and never throws
In the AI SDK, streamText returns immediately and surfaces failures on the streams. @deuz-sdk/core formalizes this: streamChat returns a StreamChatResult synchronously, the network pump starts lazily on first access of any output, and the call itself does no I/O. A failure surfaces as an error part on fullStream and rejects the usage / finishReason promises — it is never a synchronous throw. So you do not wrap streamChat(...) in try/catch; you handle the error part or await the promise.
Result fields map cleanly:
AI SDK streamText result | @deuz-sdk/core StreamChatResult |
|---|---|
textStream | textStream (AsyncIterable<string>) |
fullStream | fullStream (AsyncIterable<StreamPart>) |
usage (Promise) | usage (Promise<Usage>) |
finishReason (Promise) | finishReason (Promise<FinishReason>) |
Canonical StreamPart names
Both SDKs normalize provider SSE into a canonical delta union, but the part type strings differ. The fullStream uses these names:
| AI SDK part type | @deuz-sdk/core StreamPart type |
|---|---|
text-delta | text-delta |
reasoning / reasoning-delta | reasoning-delta |
tool-call-streaming-start / tool-call-delta | tool-call-delta |
tool-call | tool-call |
tool-result | tool-result |
step-start | step-start |
step-finish | step-finish |
source | source |
finish | finish |
error | error |
The StreamPart union is open — keep a default case in your switch, since new variants are additive. The text payload field is always text (for both text-delta and reasoning-delta). The Deuz UI wire (readDeuzStream) reframes a few of these — most notably tool-call-delta becomes tool-input-delta, and the error part carries a message string instead of an error value — so match on the UI part names when reading the wire.
Loop control: maxSteps and stopWhen
The agentic loop is bounded the same way as the AI SDK: maxSteps (default 1, i.e. single-turn) plus an optional stopWhen predicate, OR-ed together. Since 1.4 the stepCountIs / hasToolCall factories are exported (plus the usage-based totalTokensExceed / costExceeds, which the AI SDK has no equivalent of); stopWhen also accepts a plain callback you write inline, receiving { steps, stepCount, usage, costUSD }.
const result = await generateText({
model: anthropic('claude-opus-4-8'),
messages,
tools,
maxSteps: 8,
// OR-ed with maxSteps. Return true to stop after the current step.
stopWhen: ({ steps }) => steps.at(-1)?.toolCalls.some((c) => c.toolName === 'finalAnswer') ?? false,
});Per-step and usage callbacks exist as call options: onStepFinish(step), onUsage(usage, meta), and onFinish(meta). There is no separate experimental_ namespace — these are stable fields on the call options.
Code: basic stream
// Vercel AI SDK
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const result = streamText({
model: anthropic('claude-3-5-sonnet'),
messages: [{ role: 'user', content: 'Write a haiku about TypeScript.' }],
});
for await (const chunk of result.textStream) process.stdout.write(chunk);// @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'),
messages: [{ role: 'user', content: 'Write a haiku about TypeScript.' }],
});
for await (const chunk of result.textStream) process.stdout.write(chunk);
const usage = await result.usage; // resolves when the stream finishesCode: tools
The AI SDK's tool() helper becomes a plain object keyed by tool name. parameters is any Standard Schema (Zod, Valibot) or a raw JSON Schema; execute is optional (omit it for a client tool).
// Vercel AI SDK
import { generateText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const { text } = await generateText({
model: anthropic('claude-3-5-sonnet'),
messages: [{ role: 'user', content: 'Weather in Paris?' }],
maxSteps: 5,
tools: {
getWeather: tool({
description: 'Get the weather for a city',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, tempC: 22 }),
}),
},
});// @deuz-sdk/core
import { generateText } 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'),
messages: [{ role: 'user', content: 'Weather in Paris?' }],
maxSteps: 5,
tools: {
getWeather: {
description: 'Get the weather for a city',
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => ({ city, tempC: 22 }),
},
},
});Parallel execution (maxToolConcurrency, default 5), self-healing on thrown tools, immutable cache-safe history, and runaway guards are built in. See Tools and The tool loop.
Code: structured output
Your Zod schema works unchanged via Standard Schema — no adapter needed. You can also pass a raw JSON Schema object. mode ('auto' | 'json' | 'tool') replaces the AI SDK's mode / output knobs.
// Vercel AI SDK
import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { z } from 'zod';
const { object } = await generateObject({
model: anthropic('claude-3-5-sonnet'),
messages: [{ role: 'user', content: 'Capital of France as JSON.' }],
schema: z.object({ city: z.string() }),
});// @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'),
messages: [{ role: 'user', content: 'Capital of France as JSON.' }],
schema: z.object({ city: z.string() }),
// mode: 'auto', // 'json' | 'tool' to force a strategy
});On a parse/validation miss @deuz-sdk/core runs one repair retry, then throws NoObjectGeneratedError. For streaming partials, use streamObject — same options, no repair retry.
Code: UI route
The AI SDK's toDataStreamResponse() / toUIMessageStreamResponse() becomes toDeuzStreamResponse(). This is our own versioned SSE wire (x-deuz-stream: v1), not the AI SDK protocol, so it is consumed by readDeuzStream rather than useChat.
// Vercel AI SDK — app/api/chat/route.ts
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({ model: anthropic('claude-3-5-sonnet'), messages });
return result.toDataStreamResponse();
}// @deuz-sdk/core — app/api/chat/route.ts
import { streamChat } from '@deuz-sdk/core';
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) {
const { messages } = await req.json();
return toDeuzStreamResponse(
streamChat({ model: anthropic('claude-opus-4-8'), messages }),
);
}Until the React hooks ship, consume the stream on the client with readDeuzStream:
import { readDeuzStream } from '@deuz-sdk/core/ui';
const res = await fetch('/api/chat', {
method: 'POST',
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;
default:
break; // open union — keep a default case
}
}Not implemented yet
These exist in the AI SDK but are deliberately deferred in @deuz-sdk/core:
useCompletion— useuseChatwith a single-turn route, or drivereadDeuzStreamdirectly.- Svelte/Vue/Angular bindings — the wire is framework-agnostic; only React hooks ship today.
Everything else in the chat, tools (including approval), structured output (buffered and streaming), embeddings, MCP, React hooks, and UI surface has a direct equivalent above.