Tool fields
Validate model-emitted values at the tool boundary.
Prefer the live demos? Open this section in the interactive docs.
Constrained decoding can make JSON parse. The parser checks whether the values mean what they should.
JSON mode can't stop "2kg" landing in a number field (Number("2kg") → NaN, z.coerce.number() → NaN too), "1,5" losing its locale, or new Date("03/04/2025") silently picking the wrong month. Models are better at emitting "5'11\"" than 1.8034; @pascal-app/lingo/ai makes that string the reliable path.
Tool-boundary defaults (plan 020), each with a one-line escape hatch: AMBIGUOUS_NUMBER escalates to error with a did-you-mean candidate ("1,234 kg" fails; the model re-emits "1234 kg"); dateField escalates TZ_IGNORED to error and requires an explicit now for reference-dependent dates ("tomorrow", "March 5", and "at 3pm" fail with NOW_REQUIRED; requireNow: false opts out by resolving from the wall clock at field validation; fully absolute dates are unaffected); dateRangeField applies the same reference-time and timezone guards to a natural-language time slot ("2pm to 4pm", "9-5") and returns { start?, end?: ISO }; min/max bounds fail with RANGE_MIN/RANGE_MAX and appear in JSON Schema minimum/maximum plus the input description; numeric rangeField output rejects open-ended inputs with RANGE_OPEN_BOUND_NOT_ALLOWED, while output: "range" returns full QuantityRange JSON with open bounds, exclusivity, fuzzy/approximate origin, and baseUnit; lingoObject is closed (additionalProperties: false, unknown keys fail, OpenAI strict compatible; { passthrough: true } opts out); absorbed forgiveness (typos, assumed units, ambiguous dates under dayFirst) is surfaced as structured warnings on success; failures keep Standard Schema message/path and add lingo code, severity, field-input span, data, suggestions, and candidate when known; canonical numbers are float-safe.
The docs demo at /docs#for-ai canonicalizes editable model JSON such as { "weight": "2 lbs", "height": "5'11\"", "deliverBy": "next tues", "boxWeight": "3-5 kg" } with canonicalizeValues, quantityField, rangeField, and dateField.
Providers
- Vercel AI SDK: pass a field into
tool()/Output.object()with no Zod (v6/v7). - OpenAI: strict function tools via
toJSONSchema();safeParsethe args. - Anthropic:
input_schema+is_errortool results. - Google Gemini:
parametersJsonSchema; never the classic surface. - LangChain:
withStructuredOutput();canonicalizeValuesaftercreateAgent. - MCP:
lingoTool()gives you a fullregisterToolcontract.
The same lingoObject is a Standard Schema. Standard-Schema-aware libraries take it directly; raw SDKs get the same input schema through toJSONSchema().
Workflows
- Tool calls: validate and canonicalize model arguments at the boundary.
- Tool repair:
repairToolCallWith()fixes"2kg"→2with no extra model call. - Data collection: run
canonicalizeValues()over extracted records before a DB write. - Evals:
quantityMatch/dateMatchgrade unit and date equivalence. - Computer use: agents type
5'11"; the field commits1.8034.
import { generateText, Output, tool } from "ai" // ai@^6 or ai@^7
import {
dateField,
lingoObject,
optional,
quantityField,
rangeField,
} from "@pascal-app/lingo/ai"
const schema = lingoObject({
weight: quantityField({ kind: "mass", unit: "kg", min: 0, max: 500 }),
height: quantityField({ kind: "length", unit: "m" }),
deliverBy: dateField({ now }),
boxWeight: rangeField({ kind: "mass", unit: "kg" }),
tareWeight: optional(quantityField({ kind: "mass", unit: "kg", min: 0 })),
carrier: "string",
})
const createShipment = tool({
description: "Create a shipment record.",
inputSchema: schema,
execute: async (input) => warehouse.create(input),
})
const { output } = await generateText({
model,
tools: { createShipment },
output: Output.object({ schema }),
prompt: "Extract shipment details from this note.",
})
// output.weight is kg, deliverBy is ISO, tareWeight is number | null.
// AI SDK v5 needs the SDK's jsonSchema() wrapper around this schema.AI code tabs on the page:
- AI SDK:
generateTextwithOutput.object({ schema }), plustool({ inputSchema })directly on the samelingoObject. - Repair:
repairToolCallWith(spec)asexperimental_repairToolCall;experimental_repairTextis the deprecated v5 path. - Tool call:
canonicalizeValues(toolCall.args, spec); filterseverity === "error"before executing business logic. Issue spans point into the field string, not the whole JSON payload. - MCP:
lingoTool()emitsinputSchemaand wrapssafeParsein the callback so the model gets[CODE]-prefixed tool errors. - OpenAI: Chat Completions strict function tools use
toJSONSchema(schema)forparameters, thensafeParsethe tool arguments in the handler. - Anthropic:
input_schema: toJSONSchema(schema) as Anthropic.Tool.InputSchema, withis_errortool results for parse failures. - Gemini: use
parametersJsonSchema; never the classicparameters/responseSchemasurface for a closed lingo schema. - LangChain:
model.withStructuredOutput(lingoObject({...}))validates on supported model overrides;createAgent+toolStrategyvalidate shape only, so runcanonicalizeValuesyourself. - Evals:
quantityMatchanddateMatchcanonicalize both sides before scoring. - Computer use:
lingoInputaccepts agent-typed5'11"and commits hiddenheight_m=1.8034.
One schema travels across providers: Standard Schema-aware libraries can receive the lingo field directly, while raw provider SDKs get the same input side through toJSONSchema(). Keep optional tool arguments explicit with optional(field); strict providers still see a required key whose type admits null.
Eval framing: Canonicalization-rate demo on a recorded corpus, not an end-to-end LLM benchmark. When site/src/data/ai-eval.json exists at build time, the docs render acceptance rate and silent-wrong rate per category for naive vs. lingo. The two metrics are separate and never blended. When the file is absent, the eval readout renders nothing.
The site exposes /llms.txt as the agent index, /docs/<section>.md for per-topic markdown, /llms-full.txt for the complete narrative, and /llms-small.txt as the npm-shipped compressed reference. Coding agents should fetch /llms.txt first, follow section .md URLs for the topic they need, and keep user quantities as strings until lingo validates them.