Structured Outputs
Get typed JSON responses from LLMs using Zod schemas and response format modes.
Overview
Structured outputs let you constrain the LLM to return data in a specific JSON shape. Cogitator supports three response format modes and handles the provider-specific differences for you.
import { Agent } from '@cogitator-ai/core';
import { z } from 'zod';
const agent = new Agent({
name: 'extractor',
model: 'openai/gpt-4o',
instructions: 'Extract structured data from the given text.',
responseFormat: {
type: 'json_schema',
schema: z.object({
title: z.string(),
topics: z.array(z.string()),
sentiment: z.enum(['positive', 'negative', 'neutral']),
}),
},
});Response Format Modes
text (default)
The LLM responds with free-form text. This is the default when no responseFormat is set.
responseFormat: {
type: 'text';
}json
The LLM is instructed to return valid JSON, but without a specific schema. Useful when you need flexible JSON output.
const agent = new Agent({
name: 'json-agent',
model: 'openai/gpt-4o',
instructions: 'Respond with a JSON object containing your analysis.',
responseFormat: { type: 'json' },
});
const result = await cog.run(agent, { input: 'Analyze this sentence: "I love TypeScript"' });
const data = JSON.parse(result.output);json_schema
The LLM is constrained to a specific JSON structure defined by a Zod schema. This is the most powerful mode -- the output is guaranteed to match your schema.
const sentimentSchema = z.object({
text: z.string(),
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number(),
keywords: z.array(z.string()),
entities: z.array(
z.object({
name: z.string(),
type: z.enum(['person', 'organization', 'location', 'other']),
})
),
});
const agent = new Agent({
name: 'sentiment-analyzer',
model: 'openai/gpt-4o',
instructions: 'Analyze the sentiment and extract entities from the input text.',
responseFormat: {
type: 'json_schema',
schema: sentimentSchema,
},
});How Providers Handle It
Each LLM provider implements structured outputs differently. Cogitator abstracts these differences away, but understanding them helps pick the right provider.
OpenAI
OpenAI natively supports all three modes. For json_schema, it uses the response_format parameter with strict schema enforcement:
// what Cogitator sends under the hood
response_format: {
type: 'json_schema',
json_schema: {
name: 'response',
schema: { /* your Zod schema converted to JSON Schema */ },
strict: true,
},
}Anthropic
Anthropic does not have native JSON schema support. Cogitator uses a tool trick -- it creates a synthetic tool named __json_response with your schema as its parameters and forces the LLM to call it:
jsonmode: Appends "respond with valid JSON only" to the system promptjson_schemamode: Injects a tool with your schema and setstool_choiceto require it
The output is extracted from the tool call arguments, so you get the same structured response regardless of provider.
Google (Gemini)
Google supports structured outputs via responseMimeType and responseSchema:
jsonmode: SetsresponseMimeType: 'application/json'json_schemamode: Sets bothresponseMimeTypeandresponseSchema
Ollama
Ollama supports structured outputs via the format field:
jsonmode: Setsformat: 'json'json_schemamode: Passes the full JSON Schema as theformatvalue
Azure OpenAI
Azure uses the same format as OpenAI since it's backed by the same API.
Defining Schemas with Zod
Use the full power of Zod to define your output shapes:
const analysisSchema = z.object({
summary: z.string().describe('A brief summary of the content'),
categories: z
.array(z.enum(['tech', 'business', 'science', 'politics', 'entertainment']))
.describe('Relevant categories'),
metrics: z.object({
readability: z.number().min(0).max(100),
complexity: z.enum(['simple', 'moderate', 'complex']),
wordCount: z.number().int().positive(),
}),
references: z
.array(
z.object({
title: z.string(),
url: z.string().url().optional(),
relevance: z.number().min(0).max(1),
})
)
.optional(),
});Field descriptions (.describe()) are included in the JSON Schema sent to the LLM, which improves output quality.
Parsing the Output
The result.output is always a string. For structured outputs, parse it:
const result = await cog.run(agent, { input: 'Analyze this article...' });
const data = JSON.parse(result.output);Since the LLM is constrained by the schema, the parsed JSON will match your Zod type. You can validate it for extra safety:
const parsed = analysisSchema.parse(JSON.parse(result.output));Common Patterns
Classification
const classifier = new Agent({
name: 'classifier',
model: 'openai/gpt-4o-mini',
instructions: 'Classify the support ticket into a category and priority.',
temperature: 0,
responseFormat: {
type: 'json_schema',
schema: z.object({
category: z.enum(['billing', 'technical', 'account', 'feature_request', 'other']),
priority: z.enum(['low', 'medium', 'high', 'critical']),
reasoning: z.string(),
}),
},
});Data Extraction
const extractor = new Agent({
name: 'extractor',
model: 'anthropic/claude-sonnet-4-20250514',
instructions: 'Extract all contact information from the given text.',
responseFormat: {
type: 'json_schema',
schema: z.object({
contacts: z.array(
z.object({
name: z.string(),
email: z.string().email().optional(),
phone: z.string().optional(),
company: z.string().optional(),
})
),
}),
},
});