Deuz SDK 2.0 リリース — ストア、ガードレール、ハンドオフ、ゼロ設定 MCP。 2.0 の新機能
Deuz SDK
Core

Prompts, instructions & timeouts

The 1.9 call-shape ergonomics — prompt, instructions, the four timeout layers, abortSignal, and per-call capability overrides.

ドキュメント本文は英語です。ナビゲーション・検索・UI は選択した言語に従います。

Four additions in 1.9 remove the boilerplate that used to sit between you and a correct call. All are optional; every 1.8 call still compiles and behaves identically.

prompt — one user turn

Before 1.9, messages was the only way in, so the smallest possible call was a nested array literal.

prompt.ts
import { streamChat } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

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

const res = streamChat({
  model: anthropic('claude-opus-4-8'),
  prompt: 'Explain SSE in one sentence.',
});

prompt is shorthand for exactly one user turn — messages: [{ role: 'user', content: prompt }] — and is mutually exclusive with messages. It is available on streamChat, generateText, generateObject and streamObject.

Both are resolved into a canonical Message[] at the call boundary, so everything downstream (both loops, checkpoints, chat persistence, response.messages, observation) sees a plain messages array. Nothing else in the SDK knows the shorthand exists.

Passing both, or neither, is an error

generateText / generateObject reject with an InvalidRequestError before any network request. streamChat / streamObject keep their never-throw contract (G2): the error arrives as one error part on fullStream, textStream throws, partialObjectStream rejects, and usage / finishReason reject.

One deliberate asymmetry: messages: [] combined with prompt is not "both given" — an empty collection asks for nothing, so prompt wins. That keeps generic wrappers which always spread a full options bag working.

instructions — the system prompt

instructions.ts
const res = streamChat({
  model: anthropic('claude-opus-4-8'),
  instructions: 'You are terse. Answer in one sentence.',
  prompt: 'Explain SSE.',
});

instructions combines with either input shape and is placed first. A system-role message already in messages is preserved after it — the option arrives on its own structural field while history may be a replayed or user-supplied transcript, so history content cannot reorder or drop your framing.

The fold is idempotent: if the first turn is already a system message whose content is exactly instructions, no second copy is prepended. That is what makes persisting the folded history and passing the same instructions on the next turn safe — system prompts cannot stack.

// messages: [{ role: 'system', content: 'You are terse.' }, { role: 'user', … }]
// + instructions: 'You are terse.'   → unchanged, no duplicate.

There is no system option name. Adapters place instructions on their wire's dedicated system channel.

timeout

There was no per-call timeout before 1.9 — only module constants (60s to first byte, 300s total), which is a meaningless ceiling inside a serverless function with a 25-second budget.

timeout.ts
await generateText({
  model: anthropic('claude-opus-4-8'),
  prompt: 'Summarise this repo.',
  timeout: { ttftMs: 10_000, totalMs: 20_000, stepMs: 30_000, toolMs: 5_000 },
});

// A bare number is shorthand for { totalMs }:
await generateText({ model, prompt: '…', timeout: 20_000 });
LayerScopeDefault when unset
ttftMsOne model call: time to the first content byte. Cleared once content arrives, so a slow-but-alive stream is fine.60_000
totalMsOne model call: hard ceiling on that whole response.300_000
stepMsOne agentic step end-to-end — the model call plus the tool executions it triggered.unbounded
toolMsOne tool execute.unbounded
  • Only the layers you set are overridden; the others keep the table's defaults.
  • An explicit 0 disables that layer. That is how you opt out of the 300s total ceiling: timeout: { totalMs: 0 }.
  • Every timer is scheduled through deps.clock — never an ambient host timer — so tests stay deterministic.

An expiry is a failure, not a cancellation

A timeout produces a TimeoutError: an error part plus rejected usage / finishReason. It never resolves finishReason: 'aborted' — that is reserved for a user abort, which still resolves with partial usage.

Since 1.9 an expiry whose abort reason was discarded by the transport is recovered to the TimeoutError that was armed, rather than being misclassified as a user abort.

Tool.timeoutMs

Cap one slow tool without loosening the budget for every other one.

tools: {
  runBuild: {
    description: 'Run the project build.',
    parameters: buildSchema,
    timeoutMs: 120_000, // overrides timeout.toolMs for this tool only
    execute: runBuild,
  },
}

Expiry is self-healing: the execution is abandoned and the model receives an is_error tool_resultTool 'runBuild' timed out after 120000ms and was abandoned. — so every tool_use_id still gets a result and the run continues. Nothing throws out of the call.

Tool timeouts count toward the runaway-tool guard, so a tool that hangs three times running hard-stops the loop instead of burning the full budget repeatedly. One success resets the counter. (Approval denials still do not count — a policy verdict is not something the model can fix.)

abortSignal

A deprecated alias for signal, accepted purely for migration ergonomics.

An AI SDK port copies abortSignal into an options object, and because excess-property checks only fire on literals it type-checked silently inside a spread: the user pressed stop and nothing happened.

const res = streamChat({ model, prompt, abortSignal: controller.signal }); // honoured

abortSignal is now honoured everywhere signal is. If both are set, signal wins. Prefer signal; the alias is @deprecated and exists for migrations.

capabilities

Override what the SDK believes about a model it does not know yet.

An unknown model slug does not throw; it falls back to a conservative registry row whose maxOutput is 4096. So a brand-new Together / Groq / OpenRouter slug was silently truncated at 4096 output tokens, reasoning: false dropped effort, and structuredOutput: false pushed generateObject onto the tool strategy.

capabilities.ts
await generateText({
  model: ollama('some-brand-new-model'),
  prompt: '…',
  capabilities: { maxOutput: 32_000, reasoning: true, structuredOutput: true },
});

Shallow-merged over the resolved row — set only what you know. Also settable per factory via createOpenAICompatible({ capabilities }), with the per-call value winning.

This changes belief, not behaviour

capabilities overrides what the SDK believes; it cannot change what the provider does. In particular capabilities.tools is read by no adapter, so setting it neither enables nor disables tool calling.

A call that passes capabilities runs against a per-call clone of the model descriptor, so descriptor object identity differs for that call. Key and base-URL resolution are unchanged (G1 still resolves in one place).

getModelCapabilities(model)

gate-on-capabilities.ts
import { getModelCapabilities } from '@deuz-sdk/core';

const caps = getModelCapabilities(model);
if (caps.vision) showImageUpload();
if (!caps.known) showNewModelHint(); // served from the conservative fallback row

Returns the effective matrix — registry row plus any factory-level override — as a frozen copy. It never throws, and because no logger is threaded through it, a read never emits a warning either. Use it instead of hard-coding slug lists in your UI.

See also

このページの内容