Annotated metadata on the fields of an args_schema. They are
read at construction, before any request is made: routing, encoding, visibility,
key spelling and what the model is told about a field all come from the
declaration.
Path, Query, Body or Header marker is refused with a
DeclarationError naming it, at the first
call: where it belongs in the request is undeclared, and the runtime does not
guess. The one exception is a FromContext field, which is there
for the rules and is not sent unless it also carries one.
Path
path_case is applied, must
match a placeholder in the tool’s url_template — a field marked
Path() with a name the template does not mention is not sent anywhere.
Legal on any scalar field.
bool
default:"False"
Whether the value may span path segments. GitHub’s file path is the case that
needs it:
"src/charter/tool.py" is one value, not three.What a path value cannot do
The value is interpolated into the URL, so unescaped it is not a parameter at all — it is an edit to the endpoint. Path values arrive from a model that has usually just read untrusted text, so they are percent-encoded before they reach the URL:..is refused, with aToolValidationErrorraised before any request. Without this, a path could walk up the template and reach a different endpoint entirely — a different repository’s file from a tool called with the first repository’s name.?and#are encoded, so a value cannot start a query string or a fragment and add parameters the schema never declared./is encoded unless the field declaresallow_slash=True, which keeps each value inside its own segment.
Sheet1!A1:B2 — so this changes no request
that was already correct.
Query
query_format: repeated keys by default, expand[0]=x under "bracket".
None is omitted rather than sent empty.
Body
bool
default:"False"
Keep the field name in the serialised body instead of unwrapping to its
value. Required for APIs such as Gmail’s
drafts.insert, which expect
{"message": {...}} rather than the bare object.str
default:"None"
The field is the whole request body, in this media type, when it is set:
its value, after any
Format, is sent as-is with this Content-Type. For an
operation that takes its body in more than one format — Microsoft Graph’s
sendMail takes JSON or a base64 MIME document as text/plain. It excludes
every other body field, a rule the runtime derives and enforces in the view
the model fills in, and it is not counted when the other body fields decide
whether to unwrap. Not combinable with envelop. See
a body in another media type.Body() field unwraps: that field’s value
is the body. The count comes from the schema, not from which fields a
particular call populated, so the wire shape does not depend on the arguments.
A lone scalar does not unwrap. Unwrapping promotes a nested structure’s own
fields to the root of the request, and a scalar has none to promote, so it keeps
its field name: join(channel=Body()) sends {"channel": "C1"}. Lists still
unwrap — a list, unlike a scalar, is a document.
Header
Prefer: odata.maxpagesize=10, and nothing in its query or body does the same
job:
ainvoke(args, headers=...), which a model
filling in arguments cannot reach.
str
required
The header, sent as written. One token, such as
Prefer.str
default:"None"
How the value is written into the header, with
{} where it goes. Exactly one
{}. Without a template the value is the whole header.True and False are sent as true and false, and an
enum member as its value. None sends no header.
What a header value cannot do
The value comes from a model, so the header it writes is checked the way a path value is:- A line break is refused, with a
ToolValidationErrornaming the argument, before any request. A header value is one line; a value that broke it would start a second header the schema never declared. Authorization,Proxy-AuthorizationandCookiecannot be named, nor the headers httpx derives from the request (Host,Content-Length,Content-Type,Transfer-Encoding,Connection). ADeclarationErrorwhen the marker is built.- A header the tool already sends cannot be named. Building the tool refuses
a
Header()field that names its credential header (token_header, anapi_key_headerskey) or one of itsstatic_headers, and two fields that name the same header. A static value replaced by a model’s is the failure this prevents: Graph’sPrefer: outlook.body-content-typebeside a page size would silently lose one of the two.
static_headers and before a build_request
override and the host’s ainvoke(headers=...), which are more specific than
anything the model chose.
FromContext
context it passes to
ainvoke. The model never sees it: it is left
out of the schema the model fills in, and an argument of that name is refused as
undeclared. Rules read it as a field, typed like any other, which is what a rule
needs for “only this customer’s orders”:
Optional[str]
default:"None"
The key the value is read from in
context. Defaults to the field’s name.- Required unless it has a default. A call whose context lacks a required one
is refused with a
ContextErrorbefore any rule runs; one with a default takes it. A value of the wrong type is aContextErrortoo. A rule refusing because of the value is aToolValidationError, for the model to read. - Sent only if it also carries
Path(),Query(),Body()orHeader(). With one, it is a pin whose value arrives per call:Annotated[str, Query(), FromContext()]scopes a list to the host’s customer. - Top-level only, refused below it with a
DeclarationErrorwhen the view is built. A rule on a nested model reads the host’s mapping asinfo.context. - Never withheld by
Mode, never pinned or dropped. A projection narrows what the model sees, and this is not in it:keepleaves it alone. egress_maplists it as withheld with the reasoncontextand the key it is read from.
Format
str
required
A registered transform name —
"rfc822_base64", "proto_json",
"field_mask". Unknown names raise
TransformError at call time, listing
what is registered.llm_schema() becomes the transform’s semantic_type, so
Annotated[str, Format("rfc822_base64")] is a string on the wire and an
EmailContent to the model. The
built-in names are listed under transforms.
Legal alongside Body(), Query() or Path(). A hand-written build_request
replaces the generated one, and with it the automatic application of Format.
Mode
str
required
One mode, or several comma-separated —
Mode("create, update"). Whitespace is
stripped; the parsed set is available as .modes.
The three special modes are enforced whatever the tool’s mode is. Child fields
inherit their parent’s modes unless they carry a
Mode of their own.
Filtering happens when the tool is constructed, not when the prompt is built: a
hidden field is absent from the type the model is given.
The label is arbitrary, so it need not be a verb: a plan tier, a region, a
version and a scope all fit. Which means it is not only the pack author who picks
one. Tool.with_mode and
ToolSession(tools, mode=...)
resolve a tool that already ships at a label chosen per conversation, and the
three special modes are refused at every one of them. See
egress control and
the mode quick reference.
partial_of
model with every top-level field optional: the body of a
PATCH.
type[BaseModel]
required
The resource model to relax. It is not modified.
str | None
Class name for the result, and the heading it gets in this reference.
Defaults to
Partial<model>.str | None
Docstring for the result, which is what the pack reference renders as its
description.
Label needs a name to be created or
replaced and needs nothing to be patched, so the patch body is derived from the
resource rather than written out beside it:
Path / Query /
Body / Mode / Format markers all come across,
so the derived model routes and validates exactly as its source does. It only
stops demanding.
Nested models are left alone. Relaxing a whole tree would drop constraints the
API still enforces further down, and the two patch conventions disagree about
nesting anyway: JSON Merge Patch merges
a nested object where Google’s replaces it. Call partial_of again on a nested
model that really is partial too.
Cross-field rules survive, by the same path
Mode filtering uses, so partial does not mean unconstrained: a rule
of the form “a is required when b is set” still applies. Bodies are dumped
with exclude_none=True, so an unset field is omitted rather than sent as
null — which matches Google-style patch, where absent means unchanged, and
cannot express RFC 7396’s null-means-delete.Case
"camel" | "snake" | "pascal" | "kebab"
required
How this one key is spelled on the wire.
ConflictsWith
str
required
Names of the fields, as declared in Python, this one excludes.
str
A clause explaining why, appended to the message. Worth setting: “cannot be
combined” tells a model what to stop doing and not what to do instead.
400. Written as a
validator, the rule needs a list of the fields it covers,
which is a second place to keep in step: add a parameter and the list forgets it,
delete one and the list keeps naming it. Declared on the field, the fact travels
with the field.
Google Calendar’s events.list is the case this came from — syncToken is
refused beside eight other parameters, because an incremental sync continues the
query its token came from. Each of the eight says so itself:
WireName if it has
one, since the reader is a model holding the request it just sent:
format_conflicts prints every rule
a pack declares, and flags one that names a field the schema does not have —
which never fires and otherwise reads as enforced.
WireName
str
required
The key as the API’s reference spells it.
Case covers an API that is consistent in a convention Charter knows.
This covers the field that is not, and the commonest reason is an acronym.
snake_to_camel capitalises each component, so i_cal_uid becomes iCalUid
where Google Calendar documents iCalUID — and the same shape gives htmlUrl
for htmlURL, ipAddress for IPAddress. No case convention reaches those
names from a snake_case field, and alias does not either: the runtime dumps by
field name and converts the keys afterwards, so an alias set for the wire never
arrives.
iCalUID everywhere and send
iCalUid. The conventional spellings stay accepted on input.
Gloss
str
required
The sentence, as the model should read it. Appended to the field’s description
in the schema the model receives.
description carries the API’s own words, and that is
what makes a pack checkable: a description can be diffed against the reference
page, so anything that does not match is either an API change or a mistake.
Editing one to help a model ends that. The sentence the pack author wrote and the sentence the API
publishes become indistinguishable, and the next person to regenerate the field
from the docs takes the help away without knowing it was there.
A Gloss is declared beside the description and appended to it in the
LLM-facing schema only. The wire schema keeps the documented text exactly.
to_json_schema() and in every adapter built
on it:
CreateRefund.model_fields["amount"].description is still Stripe’s sentence on
its own.
Stripe’s POST /v1/refunds is the case this came from. “A positive integer in
the smallest currency unit” is Stripe’s phrase and it is correct. A 3B model
reading 15.00 off a spreadsheet sent amount=15 and refunded fifteen cents.
Nothing rejects that: units are the caller’s to get right, the request is valid,
and the API answers 200.
Reach for a constraint first.
ge, le, pattern, min_length and
ConflictsWith are checked before the request leaves, and a
gloss is only read. Write one for what no constraint can express: the unit and
its conversion, the value a model reaches for that the API reads as something
else, the field that looks optional and is not.KeyCase
body_case, query_case, path_case,
their _override forms, and Case.
static_query, static_headers and static_body keys are exempt: they are
sent verbatim. See the key case cascade.
TransportOverride
build_request callable returns. Every key is optional, and the parts
you omit are still derived from the schema. path and query are merged over
what the schema produced, key by key, and a None in query removes that key —
how an override consumes a field it turned into another parameter, as Outlook’s
get_conversation turns conversation_id into a $filter. body is merged
into an object body, and replaces any other. headers are described below.
headers override is merged over the auth headers and static_headers, and
is itself overridden by the per-call headers argument to ainvoke.
Related
- The wire contract — how the parts are assembled
- Transforms — writing the semantic-to-wire conversion
- The key case cascade