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

Pricing & Metering

Optional USD cost accounting — the Usage breakdown, onUsage, the bundled 2026 price table, the live cost stream part, and the budget guardrail.

The core never bills. Every call returns a token breakdown (Usage); turning tokens into dollars is an app concern — margins, currency, and contract rates differ per deployment. The @deuz-sdk/core/pricing module is therefore fully optional: it ships a pinned 2026 USD price table plus a PriceProvider factory you inject via deps.priceProvider, but the core never imports it and never charges automatically. Pair it with the onUsage callback to log or meter cost per request.

Since 1.7, injecting a priceProvider also unlocks two features inside the loop itself: a live cost stream part and the budget guardrail.

Usage anatomy

Every result carries a canonical Usage. streamChat exposes it as usage: Promise<Usage> (resolves when the stream finishes); generateText, generateObject, embed, and embedMany return usage: Usage directly. The shape is locked at 1.0 — the credit system needs the full cache/reasoning breakdown to compute correct cost.

interface Usage {
  inputTokens: number; // fresh (uncached) input
  outputTokens: number; // visible output text
  reasoningTokens: number; // hidden thinking; billed at the output rate
  cachedReadTokens: number; // cache-hit reads (~10% of input price)
  cacheWriteTokens: number; // 5-minute cache writes (Anthropic)
  cacheWrite1hTokens: number; // 1-hour cache writes (Anthropic)
  audioTokens?: number; // audio in/out, when billed separately
  serverToolUses?: number; // provider-executed tool calls (e.g. web searches), billed per call, not tokens
  totalTokens: number;
}

Reasoning tokens are real output tokens you don't see in the text, so cost code bills them at the output rate. Cache reads are far cheaper than fresh input; cache writes cost a premium. priceUsage handles all of this for you.

Metering: the onUsage callback

onUsage fires exactly once per result with the Usage and a UsageMeta. It exists both as a top-level call option and as a deps field — the call-level option overrides deps.onUsage, so they never both fire (the G10 rule), and your credit system is never double-charged.

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: 'Hi' }],
  onUsage: (usage, meta) => {
    // meta: { model, reason, ttftMs? }
    console.log(meta.model, usage.totalTokens, meta.reason);
  },
});

UsageMeta is intentionally cost-free — the core reports tokens, not dollars:

FieldTypeNotes
modelstringThe model id the call ran against.
reason'finished' | 'aborted' | 'error''aborted' carries partial usage; 'error' carries what was counted before the failure.
ttftMsnumber (optional)Time-to-first-token in milliseconds, when measured.
agentPathstring[] (optional)Sub-agent path when the usage came from an agentTool loop; use it to attribute cost per sub-agent.

To attach cost, compute it inside your onUsage handler from the Usage — either with priceUsage directly or a PriceProvider.

priceUsage

The one-shot estimator. Given a model id and a Usage, it returns the USD cost from a price table (the bundled 2026 table by default), or undefined when the model is unknown.

function priceUsage(
  model: string,
  usage: Usage,
  table?: PriceTable, // defaults to PRICES_2026
): number | undefined;
import { priceUsage } from '@deuz-sdk/core/pricing';

const cost = priceUsage('gpt-5.2', {
  inputTokens: 1_000_000,
  outputTokens: 1_000_000,
  reasoningTokens: 0,
  cachedReadTokens: 0,
  cacheWriteTokens: 0,
  cacheWrite1hTokens: 0,
  totalTokens: 2_000_000,
});
// gpt-5.2: input 1.25 + output 10 (USD / 1M) → 11.25

How each bucket is priced (USD per 1,000,000 tokens):

  • inputTokensinput rate.
  • outputTokens and reasoningTokensoutput rate.
  • cachedReadTokenscachedRead rate (default: 10% of input).
  • cacheWriteTokenscacheWrite rate (default: 1.25 × input).
  • cacheWrite1hTokenscacheWrite1h rate (default: 2 × input).
  • audioTokensaudio rate (default: input).
  • serverToolUsesnot priced. Provider-executed tool calls (e.g. web searches) are billed per call, not per token; priceUsage/PRICES_2026 ignore this bucket, so add any server-tool cost separately in your app.

The result is clamped to be non-negative and rounded to micro-dollars (6 decimals) to avoid float dust.

Tolerant lookup

Model lookup is forgiving so date-stamped or vendor-prefixed slugs still resolve. It tries the exact slug, then strips a trailing ISO/compact date stamp and a leading vendor/ prefix, then falls back to the longest known prefix match.

priceUsage('gpt-5.2-2025-12-11', usage); // → priced as 'gpt-5.2'
priceUsage('google/gemini-2.5-flash', usage); // → priced as 'gemini-2.5-flash'

Unknown model: no cost, no throw

A genuinely unknown model returns undefined — never a wrong charge, never an exception. Callers decide whether to fall back to a default rate, skip billing, or alert.

priceUsage('totally-made-up-model', usage); // → undefined

PRICES_2026 and ModelPrice

PRICES_2026 is a PriceTable (a Record<string, ModelPrice>) of pinned 2026 list prices. It is not authoritative — list prices drift and enterprise/Vertex/Bedrock rates differ. Treat it as a starting point and override per deployment.

interface ModelPrice {
  input: number; // fresh input, USD / 1M
  output: number; // output (and reasoning), USD / 1M
  cachedRead?: number; // default: input * 0.1
  cacheWrite?: number; // default: input * 1.25
  cacheWrite1h?: number; // default: input * 2
  audio?: number; // default: input
}

type PriceTable = Record<string, ModelPrice>;

A few rows from the bundled table:

ModelinputoutputcachedReadcacheWritecacheWrite1h
gpt-5.55300.5
gpt-5.4-nano0.21.250.02
claude-fable-51050112.520
claude-sonnet-53150.33.756
claude-opus-4-85250.56.2510
gemini-3.5-flash1.590.15
grok-4.31.252.50.125
text-embedding-3-small0.020

Embeddings are input-only — set output: 0 and the output/cache buckets contribute nothing. Where a row omits cachedRead/cacheWrite/cacheWrite1h, the defaults above apply (e.g. qwen3-max has no explicit cachedRead, so it falls back to 0.1 × 1.2 = 0.12).

Long-context tiers: a row may carry over200k: { input, output, cachedRead? } — when inputTokens + cachedReadTokens > 200_000 those rates apply instead (e.g. gemini-3.1-pro-preview bills $2/$12 up to 200k prompt tokens and $4/$18 beyond).

PriceProvider and the deps seam

For injection, build a PriceProvider with createPriceProvider and pass it as deps.priceProvider. The seam lets you swap the bundled table for a DB- or remote-backed one (its priceUsage may be async).

function createPriceProvider(options?: CreatePriceProviderOptions): PriceProvider;

interface CreatePriceProviderOptions {
  table?: PriceTable; // merged shallow over PRICES_2026 (per-model)
  margin?: number; // multiply every cost, e.g. 1.3 for a 30% markup. Default 1
}

interface PriceProvider {
  priceUsage(model: string, usage: Usage): number | undefined | Promise<number | undefined>;
  /** Optional (1.7): USD saved by prompt-cache reads — feeds `cacheSavingsUsd` on the cost part. */
  cacheSavings?(model: string, usage: Usage): number | undefined | Promise<number | undefined>;
}

How cost surfaces: the core does not put dollars on UsageMeta. The provider you inject is yours to call — typically inside the same onUsage handler, since the app owns both. The priceProvider seam exists so app-wide wiring (price table, margin) lives in one place that your callback reads. (Since 1.7 the streaming loop additionally reads the seam itself to emit cost parts and evaluate budget.usd — still estimation for display and guardrails, never billing.)

lib/deuz.ts
import { createClient } from '@deuz-sdk/core';
import { createPriceProvider } from '@deuz-sdk/core/pricing';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const priceProvider = createPriceProvider({ margin: 1.3 }); // 30% markup

export const deuz = createClient({
  apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
  deps: {
    priceProvider,
    onUsage: async (usage, meta) => {
      const usd = await priceProvider.priceUsage(meta.model, usage);
      // unknown model → usd is undefined; bill conservatively / skip
      if (usd !== undefined) await chargeAccount(meta.model, usd, usage.totalTokens);
    },
  },
});

export const anthropic = createAnthropic();

createPriceProvider performs no I/O and reads no globals, so it is safe in any edge runtime. Unknown models still yield undefined after a margin is applied — a missing price never becomes a wrong charge. Providers built with createPriceProvider implement the optional cacheSavings seam out of the box (with the same margin applied), so the live cost part carries cacheSavingsUsd without extra wiring.

Live cost stream (1.7)

When deps.priceProvider is injected, streaming calls emit a canonical cost part — the agentic loop pushes one after every step, and a single-turn stream pushes one before its finish. It rides the UI wire v2 unchanged, so a browser can render a live cost badge with zero server code:

interface CostPart {
  type: 'cost';
  costUsd: number; // CUMULATIVE for the run — cross-leg on durable resumes
  deltaUsd?: number; // this step's increment
  cacheSavingsUsd?: number; // what cache reads saved vs full-price input (needs cacheSavings)
  stepIndex?: number;
}
  • costUsd is cumulative across the whole run. On a durable resume leg, the prior legs' cost seeds the baseline, so the first cost part's deltaUsd reports this step's increment, not the whole run's total.
  • cacheSavingsUsd appears only when the provider implements the optional cacheSavings seam and the savings are positive. The standalone helper is exported too: cacheSavings(model, usage, table?) returns what the cachedReadTokens would have cost at the full input rate minus what they cost at the cached-read rate — cache writes are the investment side and are not netted.
  • No priceProvider, or an unknown model (priceUsageundefined) → no cost parts, no errors. A throwing provider logs a warning and the part is skipped.
cost-badge.ts
for await (const part of readDeuzStream(res)) {
  if (part.type === 'cost') {
    badge.textContent = `$${part.costUsd.toFixed(4)}`; // live, per step
    if (part.cacheSavingsUsd) savings.textContent = `saved $${part.cacheSavingsUsd.toFixed(4)}`;
  }
}

The budget guardrail (1.7)

budget on CommonCallOptions hard-stops the agentic loop once cumulative cost or real token usage crosses a limit:

const result = streamChat({
  model,
  messages,
  tools,
  maxSteps: 20,
  budget: { usd: 0.5, tokens: 200_000 }, // either field alone works too
});

Semantics:

  • Sugar over the costExceeds / totalTokensExceed stop conditions, with distinct stoppedBy markers: providerMetadata.deuz.stoppedBy is 'budget.usd' or 'budget.tokens', so callers can tell a budget trip from a manual stopWhen.
  • Evaluated at step boundaries — an in-flight step always completes first (the loop never truncates a model response mid-stream).
  • budget.usd needs deps.priceProvider; without one the loop warns once and the condition never fires. budget.tokens reads provider-reported cumulative usage — no estimation.
  • The streaming loop emits a typed budget-exceeded part right before the terminal finish, so UIs can react without parsing metadata: { type: 'budget-exceeded', kind: 'usd' | 'tokens', limit, value }.
  • On a durable run, budget checks see the whole run's cumulative usage across legs — a resume leg cannot reset the meter.

Continue-confirmation UI pattern

A budget stop is not an error — the turn finishes cleanly with partial work and full history. The natural UX is a confirmation that re-runs the same conversation with a raised budget:

BudgetGate.tsx
// Reducing the stream with applyUIPart (see Chat Persistence) fills
// turn.budgetExceeded when the guardrail trips.
{turn.budgetExceeded && (
  <div role="alert">
    Stopped at {turn.budgetExceeded.kind === 'usd'
      ? `$${turn.budgetExceeded.value.toFixed(2)} (limit $${turn.budgetExceeded.limit})`
      : `${turn.budgetExceeded.value.toLocaleString()} tokens`}.
    <button
      onClick={() =>
        // Raised ceiling, same conversation: include the stopped turn's
        // assistant/tool messages in `messages` and the model picks up
        // where the budget cut it off.
        send(messages, { budget: { usd: turn.budgetExceeded!.limit * 2 } })
      }
    >
      Continue anyway
    </button>
  </div>
)}

Examples

Log cost per request

Use priceUsage against the bundled table directly when you don't need app-wide injection.

import { generateText } from '@deuz-sdk/core';
import { createOpenAI } from '@deuz-sdk/core/openai';
import { priceUsage } from '@deuz-sdk/core/pricing';

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });

const { text, usage } = await generateText({
  model: openai('gpt-5.2'),
  messages: [{ role: 'user', content: 'Summarize the SDK in one line.' }],
});

const usd = priceUsage('gpt-5.2', usage);
console.log(text);
console.log(`tokens: ${usage.totalTokens}  cost: ${usd === undefined ? 'n/a' : `$${usd}`}`);

Streaming cost via onUsage

streamChat is lazy and never throws; the usage callback fires once when the stream finishes (including 'aborted' with partial usage).

import { streamChat } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';
import { priceUsage } from '@deuz-sdk/core/pricing';

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

const result = streamChat({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Explain prompt caching.' }],
  onUsage: (usage, meta) => {
    const usd = priceUsage(meta.model, usage);
    console.log(meta.reason, meta.ttftMs, usage.cachedReadTokens, usd);
  },
});

for await (const chunk of result.textStream) process.stdout.write(chunk);

createClient with a priceProvider

Bind the price provider and metering once; every call routed through the client inherits them.

import { createClient } from '@deuz-sdk/core';
import { createPriceProvider } from '@deuz-sdk/core/pricing';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const priceProvider = createPriceProvider();

const deuz = createClient({
  apiKeys: { anthropic: process.env.ANTHROPIC_API_KEY! },
  deps: {
    priceProvider,
    onUsage: async (usage, meta) => {
      const usd = await priceProvider.priceUsage(meta.model, usage);
      console.log(meta.model, usd ?? 'unknown-price');
    },
  },
});

const anthropic = createAnthropic();
const { text } = await deuz.generateText({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Hi' }],
});

Custom price-table override

Merge your own rates over the built-ins — useful for negotiated rates, a private model, or a margin. The custom table is shallow-merged per model, so built-in slugs still resolve.

import { createPriceProvider } from '@deuz-sdk/core/pricing';
import type { PriceTable } from '@deuz-sdk/core';

const myTable: PriceTable = {
  'my-private-model': { input: 2, output: 4 },
  'claude-opus-4-8': { input: 4.5, output: 22, cachedRead: 0.45 }, // negotiated rate
};

const priceProvider = createPriceProvider({ table: myTable, margin: 1.2 });

priceProvider.priceUsage('my-private-model', usage); // uses your row
priceProvider.priceUsage('gpt-5.2', usage); // still resolves from PRICES_2026

To override globally without a provider, pass your table as the third argument to priceUsage(model, usage, myTable).

On this page