Skip to main content

ai-core-structured-outputs

Type-safe JSON schema responses from LLMs using outputSchema on chat() and useChat(). Supports Zod, ArkType, and Valibot schemas. The adapter handles provider-specific strategies transparently — never configure structured output at the provider level. Pass stream:true alongside outputSchema for incremental JSON deltas + a completed typed object via the `structured-output.complete` event. Each successfully completed structured-output run adds a typed `StructuredOutputPart` to message history. partial/final derive from the most recent structured-output part after the latest user message. convertSchemaToJsonSchema() for manual schema conversion.

Informations de source

Dépôt
TanStack/ai
Dernière activité de la source
15 octobre 2026 à 09:43
Langue détectée de SKILL.md
anglais
Étoiles
3 199
Forks
366

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
ai-core/structured-outputs
description
Type-safe JSON schema responses from LLMs using outputSchema on chat() and useChat(). Supports Zod, ArkType, and Valibot schemas. The adapter handles provider-specific strategies transparently — never configure structured output at the provider level. Pass stream:true alongside outputSchema for incremental JSON deltas + a completed typed object via the `structured-output.complete` event. Each successfully completed structured-output run adds a typed `StructuredOutputPart` to message history. partial/final derive from the most recent structured-output part after the latest user message. convertSchemaToJsonSchema() for manual schema conversion.
type
sub-skill
library
tanstack-ai
library_version
0.42.0
sources
["TanStack/ai:docs/structured-outputs/overview.md","TanStack/ai:docs/structured-outputs/one-shot.md","TanStack/ai:docs/structured-outputs/streaming.md","TanStack/ai:docs/structured-outputs/multi-turn.md","TanStack/ai:docs/structured-outputs/with-tools.md","TanStack/ai:docs/structured-outputs/harnesses.md"]
# Structured Outputs > **Dependency note:** This skill builds on ai-core. Read it first for critical rules. The `useChat` patterns below build on ai-core/chat-experience — read that for the base hook surface, then come back here for the structured-output specifics. ## Setup ```typescript import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' const person = await chat({ adapter: openaiText('gpt-5.2'), messages: [{ role: 'user', content: 'John Doe, 30' }], outputSchema: z.object({ name: z.string(), age: z.number(), }), }) person.name // string — fully typed, no cast person.age // number ``` When `outputSchema` is provided, `chat()` returns `Promise<InferSchemaType<TSchema>>` instead of `AsyncIterable<StreamChunk>`. The result is fully typed. Adding `stream: true` switches the return to `StructuredOutputStream<InferSchemaType<TSchema>>` — incremental JSON deltas plus a terminal validated object. See **Pattern 3** below for direct iteration, **Pattern 4** for the `useChat` shape on the client, **Pattern 5** for multi-turn structured chats, and **Pattern 6** for harness adapters. ## Decision: which pattern fits | Building this | Use | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | One prompt in → one typed object out (script, server endpoint, CLI) | Pattern 1 (basic) or 2 (nested) | | A UI that fills in field by field as the model streams (progressive form, live card) | Pattern 4 — `useChat({ outputSchema })` | | Direct iteration of the stream in Node or tests | Pattern 3 — async iterable | | Users iterate on a structured object across multiple turns (recipe builder, ticket refinement) | Pattern 5 — multi-turn structured chat | | Tools that gather info, then return a typed object | Combine any of the above with `tools` — see ai-core/tool-calling | | A coding agent in a sandbox inspects files, then returns a typed object | Pattern 6 — harness `outputSchema` | ## Core Patterns ### Pattern 1: Basic structured output with Zod ```typescript import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' const PersonSchema = z.object({ name: z.string().meta({ description: "The person's full name" }), age: z.number().meta({ description: "The person's age in years" }), email: z.string().email().meta({ description: 'Email address' }), }) // chat() returns Promise<{ name: string; age: number; email: string }> const person = await chat({ adapter: openaiText('gpt-5.2'), messages: [ { role: 'user', content: 'Extract the person info: John Doe is 30 years old, email john@example.com', }, ], outputSchema: PersonSchema, }) console.log(person.name) // "John Doe" console.log(person.age) // 30 console.log(person.email) // "john@example.com" ``` ### Pattern 2: Complex nested schemas ```typescript import { chat } from '@tanstack/ai' import { anthropicText } from '@tanstack/ai-anthropic' import { z } from 'zod' const CompanySchema = z.object({ name: z.string(), founded: z.number().meta({ description: 'Year the company was founded' }), headquarters: z.object({ city: z.string(), country: z.string(), address: z.string().optional(), }), employees: z.array( z.object({ name: z.string(), role: z.string(), department: z.string(), }), ), financials: z .object({ revenue: z .number() .meta({ description: 'Annual revenue in millions USD' }), profitable: z.boolean(), }) .optional(), }) const company = await chat({ adapter: anthropicText('claude-sonnet-4-5'), messages: [ { role: 'user', content: 'Extract company info from this article: ...', }, ], outputSchema: CompanySchema, }) // Full type safety on nested properties console.log(company.headquarters.city) console.log(company.employees[0]?.role) console.log(company.financials?.revenue) ``` ### Pattern 3: Direct stream iteration Pass `stream: true` alongside `outputSchema` to get an async iterable of standard streaming chunks plus a completed typed object. Use this when you're a single process end-to-end — Node script, CLI, test, or a server endpoint that responds with one JSON blob. For the in-browser progressive-UI case, jump to Pattern 4 instead. ```typescript group=person-stream import { chat } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' const PersonSchema = z.object({ name: z.string(), age: z.number(), email: z.string().email(), }) const stream = chat({ adapter: openaiText('gpt-5.2'), messages: [ { role: 'user', content: 'Extract: John Doe is 30, john@example.com' }, ], outputSchema: PersonSchema, stream: true, }) for await (const chunk of stream) { if (chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete') { // Terminal event. `chunk.value.object` is complete and typed against the // schema you passed in. Validate it in the consumer when required. chunk.value.object.name // string chunk.value.object.age // number chunk.value.reasoning // string | undefined (thinking models only) } } ``` The terminal event is a `CUSTOM` chunk: `{ type: 'CUSTOM', name: 'structured-output.complete', value: { object: T, raw: string, reasoning?: string } }`. The return type of `chat({ outputSchema, stream: true })` carries `T` through, so a plain discriminated narrow (`chunk.type === 'CUSTOM' && chunk.name === 'structured-output.complete'`) is enough — no type guard helper. **Adapter coverage for streaming:** | Adapter | `outputSchema` + `stream: true` | | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | `@tanstack/ai-openai` (Responses + Chat Completions) | **Native combined mode (#605)** — schema wired into the regular `chatStream` call alongside `tools`; engine harvests JSON, no finalization round-trip | | `@tanstack/ai-anthropic` (Claude 4.5+ only) | **Native combined mode (#605)** — `output_config.format` + `tools` in one beta Messages call. Older Claude models fall back | | `@tanstack/ai-gemini` (Gemini 3.x only) | **Native combined mode (#605)** — `responseSchema` + `tools` in one `generateContentStream`. Gemini 2.x falls back | | `@tanstack/ai-grok` | **Native combined mode (#605)** — OpenAI Responses `text.format` + `tools` for grok-4.6, grok-4.5, grok-4.3, and grok-build-0.1 | | `@tanstack/ai-openrouter` | Native single-request stream (legacy `structuredOutputStream` path; per-call combined-mode lookup is a follow-up) | | `@tanstack/ai-groq` | Legacy `structuredOutputStream` only (no tools — Groq's API rejects schema + tools + stream) | | `@tanstack/ai-bedrock` | Separate native `structuredOutputStream` finalization through Converse or an OpenAI-compatible API | | `@tanstack/ai-byteplus` | Native combined mode on supported models; unsupported models emit `RUN_ERROR` | | `@tanstack/ai-cloudflare` | Native `structuredOutputStream` without tools; with tools, a separate finalization call (Workers AI models answer the tool turn in prose) | | `@tanstack/ai-claude-code` | Combined + event source — `--json-schema` on the same harness turn. Read `useChat().final`. See Pattern 6. | | `@tanstack/ai-codex` | Combined + event source — `--output-schema` on the same harness turn. Read `useChat().final`. See Pattern 6. | | `@tanstack/ai-opencode` | Combined + event source — prompt-and-parse. Read `useChat().final`. See Pattern 6. | | `@tanstack/ai-grok-build` | Combined + event source — prompt-and-parse (ACP and streaming-json). Read `useChat().final` or the `structured-output` part. See Pattern 6. | | `@tanstack/ai-acp` (`acpCompatible`) | Combined + event source — prompt-and-parse. Read `useChat().final` or the `structured-output` part. See Pattern 6. | **JSON mode (`json_object`).** Some OpenRouter and Groq models reject strict `json_schema`. For those models the adapter sends `json_object` and adds the schema as a system prompt. The model is asked for the shape but is not held to it. A caller can force JSON mode with `modelOptions` (`responseFormat: { type: 'json_object' }` on OpenRouter, `response_format` on Groq and other OpenAI-base adapters). JSON mode is never combined with tools: the schema prompt goes only on the finalization call. Only `await chat({ outputSchema })` with a Standard Schema validates the reply, so prefer Zod/Valibot/ArkType over a plain JSON Schema on these models. | All other adapters (ollama, older Claude, Gemini 2.x) | Fallback: runs non-streaming `structuredOutput`, emits one `structured-output.complete` event | **Native-combined output vs separate finalization** is signaled by the adapter's optional `supportsCombinedToolsAndSchema(modelOptions)` method. When it returns `true`, the engine wires the JSON Schema into the regular `chatStream` call and harvests the final-turn text — middleware sees the run through `beforeModel` / `modelStream` as usual, and the `'structuredOutput'` middleware phase does **not** fire. When it returns `false` (or is omitted), the engine takes the legacy finalization path: agent loop, then a separate `structuredOutput` / `structuredOutputStream` call with `'structuredOutput'` phase tagging. Consumer code is identical across providers — always read the final object off `structured-output.complete`. ### Pattern 4: useChat with outputSchema (progressive UI) Pass `outputSchema` to `useChat` and you get a `partial` field that fills in as JSON streams in, plus a `final` field that snaps to the completed typed object on the terminal event. No `onChunk` ceremony, no manual JSON accumulation, no `parsePartialJSON` calls. **Server** (same as Pattern 3, just behind an SSE endpoint): ```typescript // app/api/extract-person/route.ts (or your framework's equivalent) import { chat, toServerSentEventsResponse } from '@tanstack/ai' import { openaiText } from '@tanstack/ai-openai' import { z } from 'zod' const PersonSchema = z.object({ name: z.string(), age: z.number(), email: z.string().email(), }) export async function POST(request: Request) { const { messages } = await request.json() const stream = chat({ adapter: openaiText('gpt-5.2'), messages, outputSchema: PersonSchema, stream: true, }) return toServerSentEventsResponse(stream) } ``` **Client:** ```tsx import { useChat, fetchServerSentEvents } from '@tanstack/ai-react' import { z } from 'zod' const PersonSchema = z.object({ name: z.string(), age: z.number(), email: z.string().email(), }) function PersonExtractor() { const { sendMessage, isLoading, partial, final } = useChat({ connection: fetchServerSentEvents('/api/extract-person'), outputSchema: PersonSchema, }) return ( <div> <button disabled={isLoading}
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub