Sessions and providers
The common client is summarized below. The remaining sections describe the lower-level provider and session APIs.
Common client
instructor.Client has resource fields responses and messages. Use them without resource parentheses:
const Product = struct {
name: []const u8,
price: f64,
in_stock: bool,
};
var product = try client.responses.parse(Product, .{
.input = "Extract the product: Coffee mug costs $12.50 and is in stock.",
});
defer product.deinit();
std.debug.print("{s}: {d:.2}, in stock={}\n", .{
product.value.name,
product.value.price,
product.value.in_stock,
});
const messages = [_]instructor.Message{
.{ .role = .user, .content = "Extract the product: Coffee mug costs $12.50 and is in stock." },
};
var parsed = try client.messages.parse(Product, .{ .messages = &messages });
defer parsed.deinit();Use create for ordinary text without an output schema:
var response = try client.responses.create(.{ .input = "Say hello." });
defer response.deinit();
var reply = try client.messages.create(.{ .messages = &.{
.{ .role = .user, .content = "Say hello." },
} });
defer reply.deinit();The resource fields borrow stable heap state owned by the client. Moving the outer client does not invalidate resource handles. Do not copy an active client owner or use a handle after client.deinit(). Results may outlive the client if their backing allocator is still alive.
ResponseCreateParams requires a text input and has optional model, max_output_tokens, and temperature fields. MessageCreateParams requires messages and has optional model, max_tokens, and temperature fields. Both also accept:
extra_body: ?std.json.Value = null,
extra_headers: []const std.http.Header = &.{},
extra_query: []const instructor.QueryParam = &.{},QueryParam has name: []const u8 and value: []const u8. ClientConfig and EnvConfig accept extra_body and extra_query with the same defaults. Their existing headers field supplies default headers. Initialization deeply copies the defaults. Request values are borrowed only during the call.
Client and request JSON objects merge recursively. Request arrays and scalars replace existing values. Unknown body keys are passed through. Request query entries replace defaults by raw name, preserve repeated request names, and use RFC 3986 component encoding. Request headers override defaults, authentication, and content type case-insensitively without duplicate names.
A non-object body returns error.InvalidExtraBody. Extras cannot override model, input, or messages. Parsing also protects text.format, response_format, output_config.format, tools, and tool_choice. These conflicts return error.ReservedBodyField. Siblings such as text.verbosity and output_config.effort remain allowed. An extra stream value of true or a non-boolean returns error.UnsupportedStreaming. false is allowed. Invalid headers return error.InvalidHeader.
Both resources also offer parseWith, parseInto, and parseIntoWith. Parsing retains local validation and retries. See the client guide for ownership and parsing options, or request extras for examples and full merge rules.
Retained lower-level API
The following sections cover OpenAI, Anthropic, and Session. Request extras work across all three HTTP adapters for ordinary and typed requests, including provider SDK paths without an injected transport.
Parsed values are validated by default with jsonschema.validateValue for supported metadata.
Core design
instructor.zig uses caller-owned lifetime scopes.
Clientowns provider config and transport defaults.Sessionowns result memory for one task/batch.create()returnsTdirectly.Session.deinit()frees all parsed result allocations together.- Provider adapter owns request mutation/retry formatting.
This matches Zig style and std.Io: caller controls I/O, allocation, and lifetime.
Goals
- Generate JSON Schema from comptime Zig type.
- Send schema to provider through adapter.
- Parse provider JSON text into Zig type with
std.json. - Retry on parse or validation errors by appending provider-specific error feedback.
- Return
Tdirectly for ergonomic code. - Free all result memory through
Session.deinit(). - Validate parsed values against supported
jsonschema.zigmetadata by default.
Limits
- Full JSON Schema spec validation.
T.validate()hooks.- Streaming.
- Arbitrary root unions without an object wrapper.
- Multi-provider parity with
instructor-go. - Rich error payloads.
- Full OpenAI generated SDK correctness.
Primary user API
var client = instructor.OpenAI.init(.{
.allocator = gpa,
.io = init.io,
.api_key = api_key,
});
defer client.deinit();
var session = instructor.session(gpa, &client);
defer session.deinit();
const request: instructor.OpenAI.Request = .{
.model = "gpt-6-luna",
.messages = &.{
.{
.role = .user,
.content = "Extract the product: Coffee mug costs $12.50 and is in stock.",
},
},
};
const product = try session.create(Product, request, .{});product is valid until session.deinit().
Type-level API
pub fn Session(comptime Provider: type) type;Returned type:
pub fn Session(comptime Provider: type) type {
return struct {
allocator: std.mem.Allocator,
arena: std.heap.ArenaAllocator,
provider: *Provider,
usage: Usage = .{},
last_usage: Usage = .{},
last_text: ?[]const u8 = null,
last_raw_response: ?[]const u8 = null,
hooks: Hooks = .{},
pub fn init(allocator: std.mem.Allocator, provider: *Provider) @This();
pub fn deinit(self: *@This()) void;
pub fn create(
self: *@This(),
comptime T: type,
request: anytype,
comptime options: Options,
) !T;
pub fn createDetailed(
self: *@This(),
comptime T: type,
request: anytype,
comptime options: Options,
) !CreateResult(T);
pub fn setHooks(self: *@This(), hooks: Hooks) void;
pub fn reset(self: *@This()) void;
};
}Convenience inference helper:
pub fn session(allocator: std.mem.Allocator, provider: anytype) Session(ProviderType(provider));Possible use:
var s = instructor.session(gpa, &client);
defer s.deinit();
const product = try s.create(Product, req, .{});Why not return wrapper
Returning wrapper is safe but noisy:
var result = try instructor.create(Product, ...);
defer result.deinit();
const product = result.value;Session makes lifetime explicit once and returns T directly:
var session = instructor.session(gpa, &client);
defer session.deinit();
const product = try session.create(Product, req, .{});This is better for app code and still memory-safe.
Lifetime rule
All allocations needed by returned T live in Session arena.
const product = try session.create(Product, req, .{});
// product.name, price, and in_stock are valid here
session.deinit();
// product invalid after this pointSession.reset() frees all prior results and usage/raw debug memory. Any previously returned T becomes invalid after reset.
Client ownership
Client stores provider config and request-time defaults:
- GPA/temp allocator
std.Io- API key
- base URL override for OpenAI-compatible providers
- HTTP config
Client should not own parsed result structs. Long-lived clients must not accumulate result memory.
Example:
var client = instructor.OpenAI.init(.{
.allocator = gpa,
.io = init.io,
.api_key = api_key,
.base_url = "https://api.openai.com/v1", // override for compatible endpoints
});
defer client.deinit();Session ownership
Session owns parsed values and optional retained debug data.
var session = instructor.session(gpa, &client);
defer session.deinit();
const user = try session.create(User, user_req, .{});
const ticket = try session.create(Ticket, ticket_req, .{});Both user and ticket are freed by one session.deinit().
Use one session per task/batch/request scope. Do not use one global session for whole application unless intentionally retaining all results.
Low-level one-shot API
Core functions powering Session.create and Session.createDetailed:
pub fn createWithArena(
comptime T: type,
temp_allocator: std.mem.Allocator,
result_allocator: std.mem.Allocator,
provider: anytype,
request: anytype,
usage: ?*Usage,
comptime options: Options,
) !T;
pub fn createDetailedWithArena(
comptime T: type,
temp_allocator: std.mem.Allocator,
result_allocator: std.mem.Allocator,
provider: anytype,
request: anytype,
usage: ?*Usage,
hooks: ?Hooks,
comptime options: Options,
) !CreateResult(T);These return data allocated in caller-provided arena. Caller must ensure arena outlives returned values and slices.
Example:
var arena = std.heap.ArenaAllocator.init(gpa);
defer arena.deinit();
const product = try instructor.createWithArena(
Product,
gpa,
arena.allocator(),
&client,
req,
null,
.{},
);Object roots
Some provider structured-output modes require an object root. instructor.zig does not hide this with schema transforms. Define the object root as a Zig type and call create normally.
Root array:
const Item = struct {
task: []const u8,
};
const Items = struct {
items: []const Item,
pub const jsonschema = .{
.name = "Items",
.fields = .{
.items = .{ .description = "Extracted items." },
},
};
};
const result = try session.create(Items, req, .{});
const items = result.items;Root union:
const Next = struct {
action: Action,
};
const next = try session.create(Next, req, .{});This mirrors Zig's explicit-type style and avoids dynamic wrapper/unwrapper state.
Options
pub const Options = struct {
mode: Mode = .json_schema,
max_retries: u8 = 3,
validate: bool = true,
schema_options: jsonschema.Options = jsonschema.strict_options,
parse_options: std.json.ParseOptions = .{
.allocate = .alloc_always,
},
};
pub const Mode = enum {
json_schema,
json_object,
tool_call,
tool_call_required,
responses_tool_call,
responses_tool_call_required,
};parse_options.allocate = .alloc_always recommended so returned T does not borrow provider response text.
Modes:
| Mode | Endpoint | Behavior |
|---|---|---|
.json_schema |
Responses or Chat Completions | Provider-native structured outputs. |
.json_object |
Responses or Chat Completions | JSON mode fallback. |
.tool_call |
Chat Completions | Sends T as function tool and parses first tool-call arguments. |
.tool_call_required |
Chat Completions | Same, with forced function choice. |
.responses_tool_call |
Responses | Sends T as Responses function tool and parses first function-call arguments. |
.responses_tool_call_required |
Responses | Same, with forced function choice. |
Provider adapters may reject unsupported endpoint/mode combinations with error.UnsupportedMode.
Validation policy
After a provider response parses into T, instructor.zig validates the value with jsonschema.validateValue by default. Validation failures are retried just like parse failures, but the retry message includes concrete validation diagnostics.
Disable validation per call when needed:
const value = try session.create(T, req, .{ .validate = false });Layering:
jsonschema.zig: emit schemas and validate parsed Zig values for supported metadata.openai.zig: HTTP/API types and client.instructor.zig: schema -> provider -> parse -> validate -> retry.
Validation coverage follows jsonschema.zig; unsupported JSON Schema vocabulary remains provider-guidance-only.
Usage
pub const Usage = struct {
input_tokens: u64 = 0,
output_tokens: u64 = 0,
total_tokens: u64 = 0,
pub fn add(self: *Usage, other: Usage) void;
};Session accumulates usage across calls:
const user = try session.create(User, req1, .{});
const ticket = try session.create(Ticket, req2, .{});
std.debug.print("tokens: {}\n", .{session.usage.total_tokens});Per-call usage is available through createDetailed() and session.last_usage:
const result = try session.createDetailed(User, req, .{});
std.debug.print("call tokens: {}\n", .{result.usage.total_tokens});session.last_text and session.last_raw_response point into the session arena and are replaced by the next successful call.
Hooks
Hooks use plain function pointers and optional context.
pub const HookEvent = enum {
request_start,
response_received,
parse_error,
validation_error,
retry,
completion_done,
};
pub const HookInfo = struct {
attempt: usize = 0,
schema_name: ?[]const u8 = null,
response_text: ?[]const u8 = null,
raw_response: ?[]const u8 = null,
error_name: ?[]const u8 = null,
validation_errors: ?[]const u8 = null,
usage: Usage = .{},
};
pub const HookFn = *const fn (?*anyopaque, HookEvent, HookInfo) void;
pub const Hooks = struct {
ctx: ?*anyopaque = null,
on_event: ?HookFn = null,
};Example:
const State = struct {
fn onEvent(ctx: ?*anyopaque, event: instructor.HookEvent, info: instructor.HookInfo) void {
_ = ctx;
std.debug.print("{s} attempt={}\n", .{ @tagName(event), info.attempt });
}
};
session.setHooks(.{ .on_event = State.onEvent });Detailed result
pub fn CreateResult(comptime T: type) type {
return struct {
value: T,
text: []const u8,
raw_response: []const u8,
usage: Usage = .{},
};
}text and raw_response are copied into the result arena. With Session, they live until reset() or deinit().
Provider adapter pattern
Provider adapters are duck-typed. No virtual table or interface object required.
Provider must implement:
pub fn completeStructured(
self: *Provider,
allocator: std.mem.Allocator,
request: Request,
schema: instructor.StructuredSchema,
comptime options: instructor.Options,
) !instructor.Completion;
pub fn appendRetry(
self: *Provider,
allocator: std.mem.Allocator,
request: *Request,
retry: instructor.RetryMessage,
) !void;Provider may optionally implement:
pub fn deinitRequest(
self: *Provider,
allocator: std.mem.Allocator,
request: *Request,
) void;createWithArena() copies user request into mutable local req. Retry hooks mutate local request. If deinitRequest exists, core calls it before returning.
Provider receives allocator for temp/request-copy allocations, not parsed result allocations unless adapter intentionally writes into session arena. Returned Completion buffers are freed by core after parse.
appendRetry receives borrowed slices. If provider keeps retry.failed_response or retry.error_message after returning, it must copy them with the provided allocator and release them from deinitRequest.
Structured schema passed to providers
pub const StructuredSchema = struct {
name: []const u8,
description: ?[]const u8 = null,
schema_json: []const u8,
strict: bool = true,
};schema_json is borrowed and valid only during completeStructured call.
Provider completion returned to core
pub const Completion = struct {
text: []u8,
raw_response: []u8,
usage: Usage = .{},
pub fn deinit(self: *Completion, allocator: std.mem.Allocator) void;
};text and raw_response are owned by caller after return and allocated by allocator passed to completeStructured.
Core first parses text into a temporary per-attempt arena to avoid retaining failed-attempt allocations. On success, it parses into the result allocator using options.parse_options. Detailed results keep their own copies of the response text.
Retry message
pub const RetryMessage = struct {
failed_response: []const u8,
error_message: []const u8,
};Default parse retry text:
JSON parsing failed: <error>. Return only valid JSON matching the schema.Provider adapters decide how to append this. OpenAI-style adapters should append:
- assistant message containing failed model text
- user message containing error feedback
OpenAI provider
The adapter uses generated OpenAI request types. It constructs request headers and uses the injected HTTP transport or the SDK's raw request dispatch.
pub const OpenAI = struct {
pub const Config = struct {
allocator: std.mem.Allocator,
io: std.Io,
api_key: []const u8,
/// Override for OpenAI-compatible endpoints.
base_url: []const u8 = "https://api.openai.com/v1",
/// `.chat_completions` works with OpenRouter and many OpenAI-compatible APIs.
endpoint: Endpoint = .responses,
/// Optional OpenRouter ranking header.
http_referer: ?[]const u8 = null,
/// Optional OpenRouter ranking header.
app_name: ?[]const u8 = null,
};
pub const Request = struct {
model: []const u8,
messages: []const Message,
temperature: ?f64 = null,
max_output_tokens: ?u32 = null,
extra_body: ?std.json.Value = null,
extra_headers: []const std.http.Header = &.{},
extra_query: []const instructor.QueryParam = &.{},
};
pub const Message = struct {
role: Role,
content: []const u8,
images: []const Image = &.{},
};
pub const Image = struct {
url: []const u8,
detail: ?[]const u8 = null,
};
pub const Role = enum {
system,
user,
assistant,
};
};Provider responsibilities:
- Convert
StructuredSchemato OpenAItext.format = { type: "json_schema", name, schema, strict }for Responses API. - Convert
StructuredSchemato Chat Completionsresponse_format = { type: "json_schema", json_schema: { name, schema, strict } }for OpenRouter-compatible APIs. - Convert
StructuredSchemato Chat Completions function tools for.tool_calland.tool_call_requiredmodes. - Convert
StructuredSchemato Responses function tools for.responses_tool_calland.responses_tool_call_requiredmodes. - Convert
.json_objectto provider JSON object format. - Emit endpoint-specific token fields:
max_output_tokensfor Responses,max_tokensfor Chat Completions. - Add auth headers.
- Extract output text.
- Extract usage.
- Preserve provider HTTP/shape error status/body for diagnostics via
lastStatus()andlastErrorBody(). - Implement
appendRetryby appending messages and copying borrowed retry slices.
OpenRouter-compatible config
var client = instructor.OpenAI.init(.{
.allocator = gpa,
.io = init.io,
.api_key = init.environ_map.get("OPENROUTER_API_KEY") orelse return error.MissingApiKey,
.base_url = "https://openrouter.ai/api/v1",
.endpoint = .chat_completions,
.http_referer = "https://github.com/h0rv/instructor.zig",
.app_name = "instructor.zig",
});Free OpenRouter models can be used by setting .model in the request to the model slug.
Core flow
session.create(T, request, options)
session.createDetailed(T, request, options)
return result.value
session.createDetailed(T, request, options)
createDetailedWithArena(T, temp_allocator, session.arena.allocator(), provider, request, &session.usage, session.hooks, options)
save result.usage/text/raw_response as session.last_*
createDetailedWithArena(T, temp_allocator, result_allocator, provider, request, usage_out, hooks, options)
req = mutable copy(request)
schema = jsonschema.toolSchemaAlloc(T, temp_allocator, options.schema_options)
schema = { name, description, schema_json, strict = true }
emit request_start
for attempt in 0..max_retries:
completion = provider.completeStructured(temp_allocator, req, schema, options)
usage_out.add(completion.usage)
emit response_received
parse_check = std.json.parseFromSliceLeaky(T, temp_attempt_arena, completion.text, options.parse_options)
if parse ok and validation ok:
copy completion.text/raw_response into result_allocator
value = std.json.parseFromSliceLeaky(T, result_allocator, copied_text, options.parse_options)
emit completion_done
completion.deinit(temp_allocator)
return .{ value, text, raw_response, usage }
emit parse_error or validation_error
completion.deinit(temp_allocator)
if no attempts remain:
return error.MaxRetriesExceeded
provider.appendRetry(temp_allocator, &req, .{ failed_response, parse_or_validation_message })
emit retrycreateWithArena() follows the same parse/retry flow but returns only T and does not retain response text/raw response.
Error model and diagnostics
pub const Error = error{
MaxRetriesExceeded,
UnsupportedMode,
MissingOutputText,
InvalidProviderResponse,
};Provider adapters can return transport/provider-specific errors too. Request extras may return InvalidExtraBody, ReservedBodyField, UnsupportedStreaming, or InvalidHeader before sending a request. See request extras for the validation rules.
Zig errors have no payload, so providers expose diagnostics separately:
const value = session.create(T, req, .{}) catch |err| {
instructor.printError(err, &client);
return err;
};Low-level writer API uses std.Io.Writer:
try instructor.writeError(writer, err, &client);Provider diagnostics include optional status/body through:
pub fn diagnostic(self: *const Provider) instructor.Diagnostic;Improvements over Go implementation
- No reflection-based request mutation. Provider adapters own retry mutation.
- No runtime schema option branching for compile-time schema shape.
- Direct
Treturn with session-owned lifetime. - Client does not leak parsed results.
- Provider request types stay concrete.
- Validation not mixed into core orchestration.
Current scope
- Clean Zig
0.17.0library build. - Session-owned direct
TAPI. - Detailed result API for raw response, response text, and per-call usage.
- Hook API for request/response/parse/retry/completion events.
- Fake provider tests for success, retry, detailed result, and hooks.
- OpenAI-compatible HTTP provider for Responses and Chat Completions.
- Local validation of supported JSON Schema constraints, enabled by default.