Strict Mode
Strict mode is the LLM provider feature that grammar-constrains tool inputs and structured output to your JSON Schema — the model can't return malformed arguments or off-schema fields. @aeye reconciles each provider's flavor of strict (OpenAI, Anthropic, Google) under one config knob so the same Zod schema works everywhere.
The strict flag
Tool.strict and Prompt.strict are boolean | number:
| Value | Meaning |
|---|---|
true | Require strict. Selection filters out models that don't declare strict support — request fails if none qualify. |
false | Force lenient. Standard JSON Schema, no strict: true on the wire. |
number > 0 (default 1) | Prefer strict, accept fallback. The number is a priority — used when there are more strict-requesting items than the model's per-request budget allows. |
| omitted | Treated as 1. |
import { Tool } from '@aeye/core';
import z from 'zod';
new Tool({
name: 'lookup',
description: 'Look up a record by id',
schema: z.object({ id: z.string() }),
// strict omitted → priority 1 (best-effort)
call: async (args) => fetch(`/api/${args.id}`).then(r => r.json()),
});
new Tool({
name: 'critical-action',
schema: z.object({ /* ... */ }),
strict: true, // hard requirement
call: async (args) => { /* ... */ },
});
new Tool({
name: 'optional-tool',
schema: z.object({ /* ... */ }),
strict: 100, // very high preference — wins budget allocation
call: async (args) => { /* ... */ },
});
new Tool({
name: 'experimental',
schema: z.object({ /* ... */ }).passthrough(),
strict: false, // never strict; tolerate extra fields
call: async (args) => { /* ... */ },
});The default of 1 means existing callers get strict where the chosen model supports it and lenient elsewhere — without changing anything. Set true only when strict is non-negotiable.
How strict: <number> differs from strict: true
true participates in selection: a request with strict: true on any tool will only run against a strict-tool-capable model. If you've configured a mix of strict-capable and non-strict-capable providers and selection picks the wrong one, strict: true is the corrective lever.
<number> participates in scoring but never filters. Selection prefers strict-capable models but accepts any. At request build time, the chosen model determines the wire format:
- Model supports strict → emitted strict, with the number used as priority for slot allocation when budget is tight.
- Model doesn't support strict → emitted lenient, silent fallback.
For most apps strict: 1 (the default) is correct. Reach for strict: true when a tool must validate, and for higher priority numbers when you have many strict-flagged tools and want to control which ones win the budget on a tight provider like Anthropic.
Per-request budgets
Some providers cap the per-request strict load. Anthropic's documented limits:
- 20 strict tools per request.
- 24 total optional parameters across all strict schemas.
- 16 union-type parameters across all strict schemas.
When a request exceeds budget, @aeye walks tools in descending priority order (true → ∞, then numbers high-to-low) and degrades the over-budget tail to LENIENT silently — rather than failing the API call:
// 25 tools all wanting strict, against an Anthropic model:
const tools = Array.from({ length: 25 }, (_, i) => new Tool({
name: `tool-${i}`,
schema: z.object({ /* ... */ }),
strict: i < 5 ? 100 : 1, // first 5 high-priority, rest default
call: async () => {/*...*/},
}));
// Wire result:
// - Tools 0-4 (priority 100): all 5 ship strict.
// - Tools 5-19 (priority 1): 15 more ship strict (budget = 20 total).
// - Tools 20-24: ship lenient.
// - API call succeeds; the lenient tools just don't have grammar enforcement.OpenAI doesn't publish hard caps, so its budget is unbounded. Google Gemini also has no documented per-request cap.
Format families
A model's strict-mode JSON Schema dialect is one of three families. Each provider's strict mode is its own beast — the dialects are NOT interchangeable:
| Family | What's distinctive |
|---|---|
openai | Records → array-of-pairs, tuples → numeric-key objects, optional → T | null, all fields required. |
anthropic | Closed objects + all-required honored, but no recursive schemas, no length / numeric constraints, per-request slot budgets. |
google | Standard prefixItems, $ref: '#' recursion, propertyOrdering emitted, restricted format whitelist (date-time / date / time only). |
@aeye/core ships seven FormatDescriptor constants encoding all this: OPENAI_STRICT, ANTHROPIC_STRICT, GOOGLE_STRICT, LENIENT, plus three non-strict family aliases. See the @aeye/core README for the full descriptor table and how to register a custom one.
Wiring up the curated table
@aeye/models ships strictSupport — a hand-maintained ModelOverride[] listing the model families known to support strict (gpt-4o+, claude 4.5+, gemini 2.0+). Splat it into your AI config:
import { models, strictSupport } from '@aeye/models';
const ai = AI.with()
.providers({ openai, anthropic, google })
.create({
models,
modelOverrides: [...strictSupport],
});Without it, models default to lenient even when their underlying API supports strict. With it:
gpt-4o,gpt-4.1,gpt-5,o1/o3/o4get the OpenAI dialect.- AWS Bedrock + Claude 4.5 / Sonnet 4 / Haiku 4 / Opus 4 get the Anthropic dialect.
- OpenRouter
openai/*,anthropic/*,google/gemini-2+get their respective dialects.
You can mix it with your own overrides:
modelOverrides: [
...strictSupport,
// Your overrides:
{ modelPattern: /gpt-4/, overrides: { tier: 'flagship' } },
// Mark a custom model as strict-capable:
{ provider: 'my-provider', modelId: 'flagship', overrides: { strictFormat: 'openai' } },
],Auto-resolution and strictFormat
Each ModelInfo has a strictFormat?: 'openai' | 'anthropic' | 'google' | 'none' field:
- Set to a family name → opts the model into strict (auto-derives the
'toolsStrict'capability) and pins the dialect. - Set to
'none'→ explicitly opts out, even if some upstream source had marked the model strict-capable. - Unset → the dialect is auto-resolved at request-build time via
resolveStrictFormat(model):model.providerif it matches a family name (covers directopenai/anthropic/googleproviders).- The
[family]/...prefix ofmodel.id(coversopenai/gpt-4oon OpenRouter).
The fallback resolves the dialect but does not auto-add the 'toolsStrict' capability. The capability comes from explicit strictFormat (or from a scraper that sets it directly). This avoids the failure mode where every legacy gpt-3.5-turbo silently gets treated as strict-capable and crashes against a real strict request.
Validation roundtrip
When a strict-capable model returns its dialect's wire shape (e.g. an array-of-pairs record from OpenAI), the Prompt's validator needs to accept that shape. @aeye handles this transparently — the chosen descriptor id is pinned on the request before it's sent, and the same descriptor's strictify rewrite is applied to the schema during validation. The cache makes both calls hit the same Zod object reference.
You don't have to do anything for this to work — but if you're authoring a custom Prompt validator or post-processing tool args yourself, use strictify(schema, descriptor) from @aeye/core to get the matching shape. See the core README for the schema utility reference.
When to override
| Situation | What to do |
|---|---|
| You want every tool strict, no fallback. | strict: true on each tool, and add a strict-capable model to your config. |
| You have many tools and Anthropic is in the mix. | Default strict: 1 plus strict: <higher> on the most-important ones. The budget allocator picks the right ones to ship strict. |
| You want NO strict on a tool that has a recursive schema. | strict: false — Anthropic strict can't represent recursion, and you'd rather opt out explicitly than rely on silent fallback. |
A model in your config supports strict but isn't in strictSupport. | Add a custom override: { provider, modelId, overrides: { strictFormat: 'openai' } }. |
strictSupport marks a model as strict-capable but it's actually broken. | Override with strictFormat: 'none' to clear the capability. |
Related
@aeye/corestrict mode — the descriptor / schema-utility layer.- Tool Calling guide — strict applies to tool input schemas.
- Structured Output guide — strict applies to response schemas too.
- Models concept —
strictSupportand the curated table.