Skip to content

Schema pipeline

Each tool has one JSON schema, built by pydantic from its args model. Each provider gets a rewritten copy. You never call the transforms yourself; the renderers do.

The pipeline

flowchart TB
  A["args model<br/>(pydantic)"] --> J["spec.json_schema()"]
  IS["input_schema<br/>(imported tool)"] --> J
  J --> CP{"check_portable<br/>at registration"}
  CP -- problems --> X["SchemaNotPortable"]
  CP -- ok --> PL["to_plain<br/>inline $ref · flatten 1-item allOf<br/>drop title · const → enum"]
  PL --> ANT["Anthropic<br/>input_schema<br/>(defaults kept)"]
  PL --> NS["OpenAI strict=False<br/>(plain, defaults kept)"]
  PL --> DD["defaults → descriptions<br/>'Default: 8.'"]
  DD --> ST["OpenAI strict<br/>all required · optional nullable<br/>additionalProperties: false"]
  DD --> GM["Gemini<br/>accepted keywords only"]
Function (schema.transforms) Used by
to_plain every renderer
to_openai_parameters(schema, strict=True) chat_tool(s), responses_tool(s)
to_gemini_parameters function_declaration(s), genai_tool
to_anthropic_input_schema anthropic_tool(s)

json_schema() returns input_schema unchanged when a tool was built by spec_from_schema; otherwise the args model's schema. Every renderer first narrows the registry with for_target. One rule turns strict off: an imported schema that is not portable renders with strict: false on OpenAI and in ToolSpec.tool_def().

Strict mode, step by step

Strict mode makes every field required. An optional field becomes nullable. The model then sends null to mean "not given". The executor runs the call, and validate_args turns that null back into the default.

sequenceDiagram
  participant Py as Signature
  participant Sch as Schema
  participant LLM as OpenAI
  participant Inv as executor → validate_args
  Py->>Sch: top_k: int = 8
  Note over Sch: integer or null, required
  LLM->>Inv: top_k: null
  Inv->>Inv: drop the null
  Inv->>Py: top_k=8
Field declared as Model sends null Function receives
top_k: int = 8 dropped before validation 8
region: str | None = None kept None
query: str (required) kept ToolArgsInvalid

The rule applies at any depth: nested models, and models inside lists. Details on Strict mode and nulls.

Where defaults go

Target default keyword Argument default in the description
OpenAI strict removed yes: Default: 8.
OpenAI strict=False kept no
Gemini removed yes: Default: 8.
Anthropic kept no

Every property gets the note, at any depth — including fields of models inside lists. A default placed directly on a list item type or a union branch (which pydantic ignores) is dropped without a note.

Why validation stays safe

Validation always uses the tool's own pydantic model. A keyword dropped for a provider never weakens checking on our side.