Skip to main content
Markers are 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.
Order does not matter, and several markers may sit on one field. A field with no Path, Query or Body marker goes to the body and raises a UserWarning naming it — declare the marker rather than relying on that.

Path

Marks a path parameter. The field’s key, after 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 a ToolValidationError raised 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 declares allow_slash=True, which keeps each value inside its own segment.
Everything legal inside a segment stays literal — a Google Calendar id is an email address, a Sheets range is Sheet1!A1:B2 — so this changes no request that was already correct.

Query

Marks a query-string parameter. Lists are serialised according to query_format: repeated keys by default, expand[0]=x under "bracket". None is omitted rather than sent empty.

Body

Marks a request-body field.
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.
A schema that declares exactly one 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

Marks a field that is transformed on its way to the wire. The model fills in the semantic type; the named transform converts it.
str
required
A registered transform name — "rfc822_base64", "proto_json", "field_mask". Unknown names raise TransformError at call time, listing what is registered.
The field’s type in 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

Marks when a field is visible to the model.
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

Derives a copy 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.
One resource often serves create, update and patch, and the operations disagree about what is mandatory. Gmail’s 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:
Descriptions, constraints, and the 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

Field-level key-case override, and the highest priority in the cascade: field beats schema, schema beats endpoint, endpoint beats factory.
"camel" | "snake" | "pascal" | "kebab"
required
How this one key is spelled on the wire.

ConflictsWith

This field cannot be sent alongside the named one(s). Enforced by the runtime, so a pack author declares the rule and does not also write it.
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.
APIs state these rules in prose and answer them with a 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:
Every conflict is reported in one message rather than one per call, and each field is named the way the API names it — a WireName if it has one, since the reader is a model holding the request it just sent:
A schema where no field declares a conflict gains no validator. Declaring on the field is the right place to write a rule and the wrong place to read the set of them. 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

The exact key this field takes on the wire. Wins over every convention in the cascade, because it is not a convention: it is the name the API documents.
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.
It names the field for the model too, not just the wire — the description is the API’s own text and refers to the field by the API’s own name, so publishing a different one would ask the model to read iCalUID everywhere and send iCalUid. The conventional spellings stay accepted on input.
This is worth a marker rather than a workaround because of how it fails. Many APIs ignore a query parameter they do not recognise, so a misspelled filter is dropped, the unfiltered result comes back, and the call answers 200. Nothing raises, and the pack looks correct until someone counts the rows. Charter’s own Calendar pack sent iCalUid for as long as it existed.

Gloss

A sentence Charter adds for the model, kept out of the documented description.
str
required
The sentence, as the model should read it. Appended to the field’s description in the schema the model receives.
A gloss is a note written beside a text its writer may not alter, which is the position a pack is in. 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.
The model reads both sentences, in 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

The type of every casing setting — 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

What a 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.
A headers override is merged over the auth headers and static_headers, and is itself overridden by the per-call headers argument to ainvoke.