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.

Informações da origem

Repositório
TanStack/ai
Última atividade na origem
15 de outubro de 2026 às 09:43
Idioma detectado do SKILL.md
inglês
Estrelas
3.199
Forks
366

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
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}
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub