Introduction
A pure, web-first, multi-provider TypeScript AI SDK with zero runtime dependencies and one canonical streaming wire.
@deuz-sdk/core is a from-scratch TypeScript AI SDK that talks to Anthropic, OpenAI, xAI Grok, Google Gemini, Vertex AI, Azure, Bedrock and Yunwu through one canonical streaming protocol. It depends on no other AI SDK, ships zero runtime dependencies, and runs anywhere fetch runs — Node, Deno, Bun, and Vercel/Cloudflare Edge.
Reach for it when you want full control over the wire, deterministic and replayable behaviour in tests, and no vendor lock-in at the streaming or UI layer. Reach for something else if you want a hosted agent platform, a prompt IDE, or a batteries-included RAG-as-a-service — this is a library, and everything stateful is a seam you fill.
60 seconds to your first result
npm init -y && npm pkg set type=module # ESM, so top-level await works
npm install @deuz-sdk/coreexport ANTHROPIC_API_KEY=sk-ant-...import { generateText } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
// The SDK core never reads process.env — you read the key, you pass it in.
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const { text, usage, finishReason } = await generateText({
model: anthropic('claude-opus-4-8'),
prompt: 'Say hello in exactly three words.',
});
console.log(text);
console.log(finishReason, `${usage.inputTokens} in / ${usage.outputTokens} out`);node --experimental-strip-types hello.ts # Node 22.6+
# or: npx tsx hello.tsSwap generateText for streamChat and you get tokens as they arrive:
import { streamChat } from '@deuz-sdk/core';
// Returns SYNCHRONOUSLY. No request has been sent yet.
const res = streamChat({
model: anthropic('claude-opus-4-8'),
prompt: 'Write a haiku about TypeScript.',
});
// The request starts here, on the first pull.
for await (const chunk of res.textStream) process.stdout.write(chunk);
console.log('\n', await res.usage);That call did more than it looks like: the request was normalized to a canonical message shape, retried up to twice if the provider failed before the first byte, guarded by a time-to-first-token and a total timeout, and normalized back into a canonical delta stream before your for await saw a single character.
What to read next
Pick the row that matches what you are building. Each is two or three pages, not the whole site.
| You are building | Read, in order |
|---|---|
| A chatbot with a UI | Quickstart → UI streaming → React hooks |
| A tool-using agent | Defining tools → The tool loop → createAgent |
| A coding agent | Tools → Compute sandbox → Coding agent cookbook |
| A long-running / autonomous agent | Autonomy → Compaction → Durable runtime |
| Structured data extraction | generateObject → streamObject |
| Retrieval over your own documents | RAG → Embeddings → Memory |
| Something that must not lose state | Persistent stores → Chat persistence → Durable runtime |
| Something users can attack | Request validation → Guardrails → Client tools |
| A multi-agent system | Sub-agents → Handoffs |
| A port from another SDK | Migrating from the Vercel AI SDK |
Whatever you build, two pages pay for themselves early: Error handling (the taxonomy you branch on) and Dependencies & clients (how you make any of it testable).
The mental model
Three ideas explain almost every design decision in this SDK. If you internalize them, the API stops having surprises.
1. One canonical line
Every request is normalized to a canonical Message[] / Part[] shape before it hits a wire, and every response is normalized to a canonical delta stream (StreamPart) before any orchestration or consumer sees it. Adapters never hand a provider's raw bytes to a caller.
Request: canonical Message[]/Part[] -> adapter (one of 4 wires) -> upstream fetch
Response: upstream SSE -> robust parser -> CANONICAL DELTA STREAM
(text-delta | reasoning-delta | tool-call-delta | source | finish)
-> inference orchestration (retry / timeout / tool-loop)
-> (a) canonical stream to the consumer (b) versioned Deuz UI wireWithout this normalization, abort, retry-after-first-byte, multi-wire merging, and typed UI events are all impossible — you cannot resume or reinterpret bytes you already forwarded. The practical consequence for you: application code that switches on part.type works identically on Anthropic, OpenAI, Gemini and every OpenAI-compatible host. Keep a default case in that switch — StreamPart is an open union and new variants are added additively.
2. One seam for everything non-deterministic
The core touches only Web APIs. Everything stateful or ambient — HTTP, the clock, logging, tracing, metering, the circuit breaker, API keys, id generation — is injected through a single Dependencies object, with sensible defaults applied for you.
import { generateText } from '@deuz-sdk/core';
import { createMockModel } from '@deuz-sdk/core/testing';
// No network, no wall clock, no randomness: the same test result every run.
const { text } = await generateText({
model: createMockModel({ responses: [{ text: 'hello' }] }),
prompt: 'hi',
});That one seam is why Date.now(), Math.random(), process.env and console.* are lint errors inside the core. It is also why the same build runs unchanged on Cloudflare Workers: there is nothing ambient left to be missing. See Dependencies & clients.
3. streamChat returns synchronously and never throws (the "G2" rule)
streamChat and streamObject do no async work in the call body. They hand you a result object immediately; the network pump starts lazily on the first access of any output. A failure — including a missing API key — never becomes a synchronous throw. It arrives as an error part on fullStream, a throwing textStream, and rejected usage / finishReason promises.
const res = streamChat({ model, prompt: 'hi' }); // never throws, sends nothing yet
for await (const part of res.fullStream) {
if (part.type === 'error') { /* the only failure channel */ }
}Two consequences worth knowing before you hit them:
- You can construct and pass around a
StreamChatResultwith zero I/O cost — handy for pre-binding a client or building a response wrapper. - If nobody reads the stream, nothing runs — no request, no
onFinish, no persistence. That is whatconsume()is for, and it is the single most common serverless mistake.
The buffered calls (generateText, generateObject, embed) are ordinary promises and do reject. Wrap those in try/catch; wrap the streaming ones in an error-part check.
Why it exists
Many AI integrations become hard to test because runtime state, provider-specific stream shapes, and UI wires leak into app code. @deuz-sdk/core keeps those seams explicit: dependencies are injected, provider streams are normalized, and the UI wire is owned by the SDK.
| Principle | What it means in practice |
|---|---|
| Pure core | No environment-variable or console coupling; inference consumes replaceable seams for clock, ids, logging, metering, circuit breaker, and keys. Documented host defaults remain injectable, so tests are deterministic and replayable. |
| Edge-safe | Only Web APIs (fetch, Web Streams, TextDecoder, WebCrypto, atob/btoa). node:* and Buffer are forbidden by lint; Node-only code lives in dedicated …/node subpaths. |
| Zero deps | The chat core ships nothing in dependencies. Heavy or stateful things are optional peers or injected seams. |
| No vendor lock | Our own canonical delta stream and our own versioned UI wire. The SDK never proxies a provider's raw SSE bytes to your client. |
| Secrets never leak | API keys are masked in every log, error, and span path. This is a regression-tested invariant — see secret redaction. |
New in 2.0
Almost everything below is additive — no existing type gained a required field, every 1.9 call still compiles, and every stored checkpoint still loads. Four loud breaks are listed on What is new in 2.0: NotImplementedError is gone, two unions gained members, the streaming compaction part always carries trigger, and generateObject / streamObject now refuse mcp / guardrails / doneWhen.
- Persistent stores — SQLite, Redis and Postgres packs behind the memory / chat / session / run seams → Persistent stores
- Guardrails — pass / block / rewrite on the run's input, each tool call, and the final answer → Guardrails
- Handoffs —
handoff()transfers the whole run to another agent, history and all → Handoffs - Zero-config MCP, plus OAuth 2.0, sampling, roots, reconnect and a connection pool → MCP
compactMessages(), a rolling summary, and automatic context-overflow recovery → Compaction- Speech, transcription and video → Speech · Transcription · Video
- Eight more providers, two of them keyless → Local models · OpenAI-compat hosts
runtimeContext— request-scoped facts travel with the call instead of a closure per request
The whole surface, including what is still missing, is on one page: What is new in 2.0. The previous release's page is still there too: What is new in 1.9.
Feature overview
| Area | What you get | Page |
|---|---|---|
| Streaming chat | Lazy, never-throwing canonical stream with usage and finish-reason promises | streamChat |
| Text generation | Buffered single-turn or multi-step text | generateText |
| Structured output | Schema-typed objects (Standard Schema or JSON Schema) with auto json/tool strategy | generateObject |
| Structured output (streaming) | Progressive DeepPartial<T> via partialObjectStream with a validated final object | streamObject |
| Embeddings | embed / embedMany with auto-batching and concurrency caps | Embeddings |
| Agentic tools | Parallel, self-healing tool loop with runaway guards | Tools and Tool loop |
| Memory | mem0 extract/reconcile pipeline over a vector or Obsidian-markdown store | Memory |
| RAG | MIME sniff, chunkers, dense + lexical (BM25) hybrid retrieval | RAG |
| Skills | SKILL.md parser with progressive disclosure | Skills |
| MCP | Zero-config servers in the loop, OAuth 2.0, sampling, roots (HTTP/SSE edge-safe; stdio Node-only) | MCP |
| Guardrails (2.0) | onInput / onToolCall / onOutput — pass, block or rewrite, reported on the stream | Guardrails |
| Handoffs (2.0) | Transfer the run to another agent, history and all | Handoffs |
| Persistent stores (2.0) | SQLite / Redis / Postgres behind the four storage seams | Persistent stores |
| Autonomy (1.8) | Workspace memory, CodeAct sandbox, planner/verifier, browser tools, background runs, live plan/activity feed | Autonomy |
| Image generation | Sync OpenAI-compatible generation, async Midjourney, Yunwu relay | Image generation |
| Speech / transcription (2.0) | generateSpeech (OpenAI, ElevenLabs) and transcribe (OpenAI, Deepgram) | Speech · Transcription |
| Video (2.0) | Async submit → poll → download over the OpenAI Videos shape | Video |
| UI streaming | toDeuzStreamResponse (server) and readDeuzStream (client) | UI streaming |
| React hooks | useChat (client-tool round-trips + approval pauses + live plan/activity) and useObject over the Deuz UI wire | React hooks |
| Middleware | wrapModel with logging, caching, PII redaction, injection guard | Middleware |
| Pricing | Optional token-to-USD cost table | Pricing |
Supported providers
All providers normalize to the same canonical stream, so application code is provider-agnostic.
| Provider | Subpath | Notes |
|---|---|---|
| Anthropic | @deuz-sdk/core/anthropic | Messages API (/v1/messages) |
| OpenAI | @deuz-sdk/core/openai | Chat Completions and Responses API + openaiEmbedding |
| Azure OpenAI | @deuz-sdk/core/azure | Deployment URL + api-key / Entra Bearer |
| Amazon Bedrock | @deuz-sdk/core/bedrock | Mantle OpenAI-compat (Bearer API key; no SigV4 SDK) |
| xAI Grok | @deuz-sdk/core/xai | OpenAI-compatible wire |
| Google Gemini | @deuz-sdk/core/google | Compat (createGoogle) and native generateContent (createGoogleNative) + googleEmbedding |
| Vertex AI | @deuz-sdk/core/vertex | Claude and Gemini on Vertex with OAuth2 Bearer auth |
| Voyage AI | @deuz-sdk/core/voyage | Embeddings |
| Yunwu | @deuz-sdk/core/yunwu | Unified relay — chat, image, embed at /v1, Midjourney at the root |
| OpenAI-compat hosts | @deuz-sdk/core/providers | Mistral, DeepSeek, Qwen, Kimi (createKimi / Moonshot), Groq, Together, OpenRouter, Cerebras, Fireworks, GLM, MiniMax + 2.0's Perplexity, Cohere, DeepInfra, NVIDIA NIM, SambaNova, Hyperbolic + Azure/Bedrock re-exports + createProviderRegistry |
| Local models (2.0) | @deuz-sdk/core/providers | Ollama and LM Studio — keyless, localhost by default |
That is 29 built-in provider ids across four chat wires. A provider factory returns a tiny LanguageModel descriptor; the model registry is the single source of truth for per-model capabilities (vision, tools, reasoning, structured output, caching, native PDF, audio, context window). Unknown model slugs do not throw — they fall back to conservative defaults and record a warning, so new model releases work without a code change.
Quality bar
- Over 1,800 tests across 113 files (vitest with golden-replay SSE fixtures and deterministic mock models — no real network), plus a separate type-contract lock run with
--typecheck tscstrict (moduleResolution: "Bundler",verbatimModuleSyntax,noUncheckedIndexedAccess)eslintwith edge-safety enforced,publint --strict, andattw(Are The Types Wrong) all green- Dual ESM + CJS build with
.d.tsfor every subpath export
Next steps
- Installation — runtimes, optional peers, and how to pick a subpath
- Quickstart — a complete working example end to end
- streamChat — the streaming entry point in depth