Part of the lingo docs (natural-language quantities, units, dates parser) — index: https://lingo.pascal.app/llms.txt

## Tool fields

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()`; `safeParse` the args.
- Anthropic: `input_schema` + `is_error` tool results.
- Google Gemini: `parametersJsonSchema`; never the classic surface.
- LangChain: `withStructuredOutput()`; `canonicalizeValues` after `createAgent`.
- MCP: `lingoTool()` gives you a full `registerTool` contract.

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"` → `2` with no extra model call.
- Data collection: run `canonicalizeValues()` over extracted records before a DB write.
- Evals: `quantityMatch` / `dateMatch` grade unit and date equivalence.
- Computer use: agents type `5'11"`; the field commits `1.8034`.

~~~ts
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: `generateText` with `Output.object({ schema })`, plus `tool({ inputSchema })` directly on the same `lingoObject`.
- Repair: `repairToolCallWith(spec)` as `experimental_repairToolCall`; `experimental_repairText` is the deprecated v5 path.
- Tool call: `canonicalizeValues(toolCall.args, spec)`; filter `severity === "error"` before executing business logic. Issue spans point into the field string, not the whole JSON payload.
- MCP: `lingoTool()` emits `inputSchema` and wraps `safeParse` in the callback so the model gets `[CODE]`-prefixed tool errors.
- OpenAI: Chat Completions strict function tools use `toJSONSchema(schema)` for `parameters`, then `safeParse` the tool arguments in the handler.
- Anthropic: `input_schema: toJSONSchema(schema) as Anthropic.Tool.InputSchema`, with `is_error` tool results for parse failures.
- Gemini: use `parametersJsonSchema`; never the classic `parameters`/`responseSchema` surface for a closed lingo schema.
- LangChain: `model.withStructuredOutput(lingoObject({...}))` validates on supported model overrides; `createAgent` + `toolStrategy` validate shape only, so run `canonicalizeValues` yourself.
- Evals: `quantityMatch` and `dateMatch` canonicalize both sides before scoring.
- Computer use: `lingoInput` accepts agent-typed `5'11"` and commits hidden `height_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.

Human docs: https://lingo.pascal.app/docs#for-ai