Type-safe JSON responses with Pydantic, Zod, or Effect schemas
LLMs generate text. Applications need data structures. Structured outputs bridge this gap—define a schema (Pydantic in Python, Zod or Effect Schema in TypeScript), and the Dedalus SDK ensures responses conform with full type safety.This is essential for building reliable applications. Instead of parsing free-form text and hoping for the best, you get validated objects that your code can trust.
import Dedalus from 'dedalus-labs';import { zodResponseFormat } from 'dedalus-labs/helpers/zod';import { z } from 'zod';const client = new Dedalus();const Event = z.object({ name: z.string(), city: z.string(), date: z.string(),});const EventsResponse = z.object({ query: z.string(), events: z.array(Event),});async function main() { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [ { role: 'user', content: 'Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).', }, ], response_format: zodResponseFormat(EventsResponse, 'events_response'), mcpServers: ['windsor/ticketmaster-mcp'], // Discover events via Ticketmaster }); console.log(completion.choices[0]?.message.parsed);}main();
TypeScript: same example using Effect Schema
import Dedalus from 'dedalus-labs';import { effectResponseFormat } from 'dedalus-labs/helpers/effect';import * as Schema from 'effect/Schema';const client = new Dedalus();const Event = Schema.Struct({name: Schema.String,city: Schema.String,date: Schema.String,});async function main() {const completion = await client.chat.completions.parse({model: 'openai/gpt-5.2',messages: [{role: 'user',content:'Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).',},],response_format: effectResponseFormat(Schema.Struct({ query: Schema.String, events: Schema.Array(Event) }),'events_response',),mcpServers: ['windsor/ticketmaster-mcp'],});console.log(completion.choices[0]?.message.parsed);}main();
Use this when you want streaming UX and a typed final result.
Streaming helpers differ by language:
Python: use .stream(...) as a context manager and read typed stream events.
TypeScript: stream tokens with create({ stream: true, ... }), then validate the final JSON with Zod/Effect.
import asynciofrom dedalus_labs import AsyncDedalusfrom dotenv import load_dotenvfrom pydantic import BaseModelload_dotenv()class Event(BaseModel): name: str city: str date: strclass EventsResponse(BaseModel): query: str events: list[Event]async def main(): client = AsyncDedalus() # Use context manager for streaming async with client.chat.completions.stream( model="openai/gpt-5.2", messages=[{ "role": "user", "content": ( "Return 3 upcoming basketball events near San Francisco as JSON. " "Use ISO dates (YYYY-MM-DD)." ), }], response_format=EventsResponse, mcp_servers=["windsor/ticketmaster-mcp"], ) as stream: # Process events as they arrive async for event in stream: if event.type == "content.delta": print(event.delta, end="", flush=True) elif event.type == "content.done": # Snapshot available at content.done (typed) print(f"\nParsed events: {len(event.parsed.events)}") # Get final parsed result final = await stream.get_final_completion() parsed = final.choices[0].message.parsed print(f"\nFinal events: {len(parsed.events)}")if __name__ == "__main__": asyncio.run(main())
import Dedalus from "dedalus-labs";import { zodResponseFormat } from "dedalus-labs/helpers/zod";import { z } from "zod";const client = new Dedalus();const Event = z.object({ name: z.string(), city: z.string(), date: z.string(),});const EventsResponse = z.object({ query: z.string(), events: z.array(Event),});async function main() { const stream = await client.chat.completions.create({ model: "openai/gpt-5.2", messages: [ { role: "user", content: "Return 3 upcoming basketball events near San Francisco as JSON. Use ISO dates (YYYY-MM-DD).", }, ], response_format: zodResponseFormat(EventsResponse, "events_response"), mcpServers: ["windsor/ticketmaster-mcp"], // Discover events via Ticketmaster stream: true, }); // Stream output to the user while collecting it for parsing. let text = ""; for await (const chunk of stream) { const delta = chunk.choices?.[0]?.delta?.content; if (delta) { process.stdout.write(delta); text += delta; } } // If you need a typed object, parse the final JSON text. // (In production, use robust JSON extraction if your model outputs any extra text.) const parsed = EventsResponse.parse(JSON.parse(text)); console.log(`\nParsed events: ${parsed.events.length}`);}main();
Define type-safe tools with automatic argument parsing:
import asynciofrom dedalus_labs import AsyncDedalusfrom dotenv import load_dotenvfrom pydantic import BaseModelload_dotenv()class SearchEventsArgs(BaseModel):city: strmonth: strmax_results: int = 5async def main():client = AsyncDedalus() tools = [ { "type": "function", "function": { "name": "search_events", "description": "Search for events in a city during a month.", "parameters": { "type": "object", "properties": { "city": {"type": "string"}, "month": {"type": "string", "description": "YYYY-MM"}, "max_results": {"type": "integer", "default": 5}, }, "required": ["city", "month"], "additionalProperties": False, }, "strict": True, } } ] completion = await client.chat.completions.parse( model="openai/gpt-5.2", messages=[{ "role": "user", "content": "Call search_events for San Francisco in 2026-01.", }], tools=tools, tool_choice={"type": "tool", "name": "search_events"}, ) message = completion.choices[0].message if message.tool_calls: tool_call = message.tool_calls[0] print(f"Tool called: {tool_call.function.name}") print(f"Parsed args: {tool_call.function.parsed_arguments}")if **name** == "**main**":asyncio.run(main())
import Dedalus from 'dedalus-labs';import { zodFunction } from 'dedalus-labs/helpers/zod';import { z } from 'zod';const client = new Dedalus();const SearchEventsTool = zodFunction({ name: 'search_events', parameters: z.object({ city: z.string(), month: z.string(), // YYYY-MM max_results: z.number().optional(), }), description: 'Search for events in a city during a month.', function: (args) => { // Your tool implementation would go here. // For docs, we return a placeholder JSON string. return JSON.stringify({ events: [], query: `${args.city} ${args.month}`, }); },});async function main() { const completion = await client.chat.completions.parse({ model: 'openai/gpt-5.2', messages: [{ role: 'user', content: 'Call search_events for San Francisco in 2026-01.' }], tools: [SearchEventsTool], // Force a deterministic tool call (useful for examples/tests). tool_choice: { type: 'tool', name: 'search_events' }, }); const toolCall = completion.choices[0]?.message.tool_calls?.[0]; if (toolCall) { console.log(`Tool called: ${toolCall.function.name}`); console.log(`Arguments: ${JSON.stringify(toolCall.function.parsed_arguments)}`); }}main();
If you need deterministic tool calling, set tool_choice to one of the object variants:
{ type: 'auto' } (model decides), { type: 'any' } (require a tool call), { type: 'tool', name: 'search_events' } (require a specific tool), { type: 'none' } (disable tools). Passing the OpenAI string form (e.g. tool_choice: 'required') will fail schema validation with a 422.
TypeScript: tool parameters with Effect Schema
import { effectFunction } from 'dedalus-labs/helpers/effect';import * as Schema from 'effect/Schema';const SearchEventsTool = effectFunction({name: 'search_events',parameters: Schema.Struct({city: Schema.String,month: Schema.String, // YYYY-MMmax_results: Schema.NullOr(Schema.Number),}),description: 'Search for events in a city during a month.',});
Tool parameters must be an object schema (use Schema.Struct({ ... })).
The Runner supports response_format with automatic schema conversion:
import asynciofrom dedalus_labs import AsyncDedalus, DedalusRunnerfrom dotenv import load_dotenvfrom pydantic import BaseModelload_dotenv()class Event(BaseModel):name: strcity: strdate: strclass EventsResponse(BaseModel):query: strevents: list[Event]def as_bullets(items: list[str]) -> str:"""Format items as a bulleted list."""return "\n".join(f"• {item}" for item in items)async def main():client = AsyncDedalus()runner = DedalusRunner(client) result = await runner.run( input=( "Find me the nearest basketball games in January in San Francisco using Ticketmaster. " "Then call as_bullets with a list of items (one per event: name, city, date)." ), model="anthropic/claude-opus-4-5", mcp_servers=["windsor/ticketmaster-mcp"], # Discover events via Ticketmaster tools=[as_bullets], response_format=EventsResponse, max_steps=5, ) print(result.final_output)if **name** == "**main**":asyncio.run(main())
import Dedalus from 'dedalus-labs';import { DedalusRunner } from 'dedalus-labs';const client = new Dedalus();function asBullets(items: string[]): string { return items.map((item) => `• ${item}`).join('\n');}async function main() { const runner = new DedalusRunner(client, true); const result = await runner.run({ model: 'anthropic/claude-opus-4-5', input: 'Find me the nearest basketball games in January in San Francisco using Ticketmaster. Then call asBullets with a list of items (one per event: name, city, date).', mcpServers: ['windsor/ticketmaster-mcp'], // Discover events via Ticketmaster tools: [asBullets], maxSteps: 5, }); console.log((result as any).finalOutput);}main();
.create() expects a plain JSON Schema object. Don’t pass a Pydantic model, Zod schema, or Effect
schema directly.
“Streaming + typed output” is language-dependent: - Python: .stream(...) yields typed events
and a typed final snapshot. - TypeScript: stream tokens and validate the final JSON with
Zod/Effect.
The Dedalus SDK’s .parse() and .stream() methods work across all providers. Schema enforcement varies:Strict Enforcement (CFG-based, schema guarantees):
You can use .parse() and .stream() with models from any provider. In practice, you only change model—everything else stays the same.For a full list of model IDs, see the providers guide.
import { effectResponseFormat, effectFunction } from 'dedalus-labs/helpers/effect';// For response schemaseffectResponseFormat(MyEffectSchema, 'schema_name')// For tool definitionseffectFunction({ name: 'tool_name', description: 'What the tool does', parameters: MyEffectParametersSchema, function: (args) => { ... },})
Using @effect/schema (deprecated upstream)
If you still use @effect/schema, schemas from @effect/schema/Schema also work with helpers/effect.You still need to install effect (the Dedalus SDK uses effect/JSONSchema and effect/Schema for conversion + validation).Prefer effect/Schema for new code.