Tool binds a Pydantic schema to one HTTP endpoint and executes it in your
process. Build one directly for a single endpoint; build them through
the factories when several endpoints share a base URL,
an auth method and a casing convention.
Tool
Identity and routing
str
required
The tool’s name, as the model sees it and as observability records report it.
"GET" | "POST" | "PATCH" | "PUT" | "DELETE"
required
HTTP method.
str
required
Path relative to
base_url, with a placeholder per Path() field —
"gmail/v1/users/{userId}/messages/send". Placeholder names are matched
against the path keys after casing is applied.Type[BaseModel]
required
The schema — the contract. Markers on its fields decide routing, encoding,
visibility and key casing.
str | Callable[[], str]
required
The API’s base URL. A callable is resolved on every request, which is what a
single-tenant-per-installation host needs — a Shopify store, a Zendesk
account. The host must never be a schema field.
str
default:"\"\""
What the tool does, written for the model. Appears in
to_json_schema().Optional[str]
default:"None"
Short user-facing phrase for an approval UI —
"Send an email". Metadata; the
runtime does not read it.Auth
Exactly one ofapi_key_headers and credential_provider is required. Passing
both, or neither, raises DeclarationError at build time.
Dict[str, str] | Callable[[], Dict[str, str]]
default:"None"
Headers injected on every request. A callable is resolved per request, which
is how a credential can arrive after the tools are built. A literal mapping
with an empty value is rejected at build time rather than at the first 401.
Optional[CredentialProvider]
default:"None"
Supplies the bearer token, fetched on every call. See
Credentials.
Optional[str]
default:"None"
The identifier passed to
get_credentials(provider), and the peer.service
label on every ToolCall. One provider implementation can serve several APIs.Optional[Sequence[str]]
default:"None"
The scopes this tool needs. Metadata for consent screens and approval UIs —
Charter never requests them. Read back by
scopes_for. Stored as a list; None becomes
[].int
default:"10"
How dead a bearer token must be before the runtime refuses to send it. A last
guard against sending something already expired, not a refresh policy — see
OAuth2Client, which renews well before
this.Optional[Iterable[int]]
default:"None"
Response statuses that mean “your credential”, raising
CredentialError
rather than APIError. Defaults to {401}. Declare {401, 403} for an API
that means “your token” by 403; most mean “not allowed”, which is an
APIError. Stored as a frozenset.Casing and encoding
"camel" | "snake" | "pascal" | "kebab"
default:"\"camel\""
Case convention for body keys.
"camel" | "snake" | "pascal" | "kebab"
default:"\"snake\""
Case convention for query keys.
"camel" | "snake" | "pascal" | "kebab"
default:"\"snake\""
Case convention for path keys.
"json" | "form"
default:"\"json\""
"form" sends application/x-www-form-urlencoded with bracket notation for
nested values — what Stripe, Twilio and OAuth2 token endpoints expect."repeat" | "bracket"
default:"\"repeat\""
"repeat" sends a list as repeated keys (labelIds=A&labelIds=B);
"bracket" sends expand[0]=A.Optional[Dict[str, Any]]
default:"None"
Query parameters sent verbatim on every request. Keys are not case-converted
and do not appear in the LLM schema.
Optional[Dict[str, str]]
default:"None"
Headers sent verbatim on every request — an Azure
api-version, a
Notion-Version.Optional[Dict[str, Any]]
default:"None"
Body keys sent verbatim on every request. The GraphQL query document is the
case this exists for: the constant belongs to the operation, so it is declared
per tool.
Behaviour
Optional[str]
default:"None"
The tool’s mode, which decides which
Mode-marked fields survive into the LLM
schema. See the mode system.Optional[Callable[[BaseModel], TransportOverride]]
default:"None"
Escape hatch: a function returning a
TransportOverride that replaces the
path, query, body or headers derived from the schema. When omitted, one is
generated from args_schema — including the Format transforms, which is why
a hand-written build_request takes over responsibility for them.Optional[ResponseHandler]
default:"None"
Async callable that reshapes the payload before it is returned. Runs only on a
success — HTTP and envelope failures have already raised. A non-async callable
raises
DeclarationError at build time. See
response handling.Optional[Pagination]
default:"None"
Where this API keeps its cursor. Declares the location; does not loop. See
Pagination.int
default:"20"
Request timeout, in seconds.
Optional[CallSink]
default:"None"
Receives a
ToolCall after every
invocation, successful or not. There is no default sink and no ambient
registry: a tool reports to whoever was named when it was built, or to nobody.Metadata
Optional[int]
default:"None"
What one call costs against the provider’s own rate limit, in that provider’s
units — Gmail bills
messages.send at 100 and messages.list at 5. Nothing
in the runtime enforces it; it is the input a budget policy needs.Optional[str]
default:"None"
Link to the API’s own rate-limit documentation.
Attributes
Every constructor parameter above is readable as an attribute of the same name, with three that are normalised on the way in.List[str]
Always a list.
None becomes [].Optional[Dict]
Copies, so the caller’s dict cannot be mutated through the tool.
Optional[FrozenSet[int]]
A
frozenset, or None when the default {401} applies.Type[BaseModel]
The schema the tool was declared with — the full one, including
Mode("response_only") fields the model never sees.Methods
Tool.llm_schema
args_schema in two ways:
Mode("response_only") and Mode("disabled") fields are gone, and Format
fields carry their semantic type — EmailContent rather than a base64 string.
Derived once, on first use, and then memoised.
Tool.prepare pays that cost at startup instead
of inside a request.
Tool.to_json_schema
name, description, and
parameters from the LLM schema’s model_json_schema().
Generated once and copied out, so an adapter may run it inside the turn loop.
The first call derives the view if nothing else has, which on a recursive schema
takes about 1.8 seconds. This method is synchronous, so it blocks whichever
thread calls it. Tool.prepare moves that to
startup.
Tool.prepare
ainvoke derives what it needs on a worker
thread if this was never called, so skipping it costs latency on one call rather
than blocking the event loop. Nothing can do that for a synchronous caller:
to_openai_tools is documented to run inside the turn loop and
calls to_json_schema, which blocks the thread it is on.
Constructing a ToolSession already does
this for every tool you hand it. It sizes each schema to decide what to defer,
and sizing one builds it. That is 2.5s for Linear’s 128 tools, on whichever
thread constructs the session, so build the session at startup rather than per
request. progressive=False skips the sizing and leaves the tools cold.
A non-zero
schema_ms on a call in production means a
tool nobody prepared.
Tool.derived
extra="forbid". A projection can only remove, so an argument outside it
raises ToolValidationError before a
request is built.
keep selects within the sibling group it names, leaving the rest of the schema
alone. drop removes a path. pin removes a path from the view the model fills
in and keeps it in the one the runtime executes, so the field is neither visible
to the model nor reachable by it, and its value takes the same Format
transform, body unwrapping, key casing and escaping a supplied one would.
Selectors are dotted paths, unambiguous field names, or the model class a field
is annotated with. Raises DeclarationError
when a selector names nothing, names several things, or would drop a field the
API requires.
Projections compose. Both keep and pin appear in
egress_map().
Tool.paths
Tool.derived can name, one level at a
time. Pass a prefix to drill down, depth=None for the whole subtree. Paths this
tool already pruned are gone, along with everything beneath them.
bool
default:"False"
Price each path instead of naming it. Returns
PathCost pairs, most expensive first.body.requests.replace_all_text from a tool priced at 8,183 tokens
leaves one of 7,822, which is its reported 361 exactly.
Only the level being returned is priced, one schema generation per path. Drill
down with under rather than pricing a whole subtree at once.
Four properties to read the numbers by. Each is something drop really does,
not noise in the measurement:
- Costs do not sum to the total. Where two fields share a
$def, dropping either one alone leaves it in place, so both price cheap and dropping both is worth more than the sum. The 33 members of the DocsRequestunion price at 6,827 between them; the branch they sit in costs 7,807. - A negative cost means the drop makes the tool larger. Pruning inside a model
several siblings share splits one
$definto per-path copies. Droppingbody.requests.insert_text.location.indexturns oneLocation_LLMinto two and takes the tool from 8,183 tokens to 8,435. - A cost of zero means the path is not in the view at all, because
Modealready removed it.events_insertpricesevent.i_cal_uidat 0 undermode="write";events_import, which reveals the field, prices the same path at 103. A projection naming a zero-cost path is inert. - A required path is priced even though
dropwould refuse it. The price is what says whetherpinis worth reaching for.
Drilling in with
under does not reopen a cycle. The models passed through to
reach under are still held against the walk, so a field whose type is already
its own ancestor is listed once, wherever you ask from. On a recursive Notion
filter, paths("body.filter") lists and_ and paths("body.filter.and_")
lists nothing, because and_ is that same filter.This is the listing only. Write such a path out and
derived still resolves it, and keep still
prunes its siblings. What it buys you is the negative-cost case above: pruning
inside a model its siblings share splits one $def rather than removing
anything, so the tool comes back larger.PathCost
paths(by_cost=True). A NamedTuple, so it unpacks as a pair and
reads as one.
Tool.ainvoke
Optional[Dict[str, Any]]
default:"None"
Positional-only. Merged with
**kwargs, which win on a conflict.Optional[Mapping[str, str]]
default:"None"
Headers the host application decides for this one call — an
Idempotency-Key, a Stripe-Account, a correlation id. Keyword-only and
structurally separate from args, so a model filling in tool arguments can
never set a header. Charter generates none of these values.Optional[httpx.AsyncClient]
default:"None"
Reuse one client across calls. When omitted, a client is created and closed
per call.
headers and client shadow schema fields of those names. Pass such a field in
the positional dict.
Every call is measured, including the ones that fail. See on_call and
observability.
Tool.invoke
ainvoke, run with asyncio.run. Calling it from a thread that
already has a running event loop raises RuntimeError naming the tool; inside
async code, await ainvoke instead. It takes no client, since the loop that
would own one does not outlive the call.
Raises
Example
Related
- The wire contract — body format, static parameters, per-call headers
- The mode system — what the model is allowed to see
- Projections — narrowing a tool you did not declare
- Adapters — the LangChain, MCP and OpenAI views of this object