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 or Body marker goes to the body and raises a UserWarning
naming it — declare the marker rather than relying on that.
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.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.
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. 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 each one
present replaces what the schema would have produced for that part of the
request; the parts you omit are still derived from the schema.
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