Skip to main content
Two of this library’s claims are, until someone measures them, only claims:
  • a pack hands the model a fraction of what the API actually sent
  • the boundary adds no meaningful latency of its own
ToolCall is those two claims as numbers, recorded on every invocation — including the ones that fail, and the ones that never leave the process. All of it is local. A record goes to the on_call sink you name and to the charter logger; there is no default sink, no phone-home, and no history. Comparing this week’s numbers against last week’s needs storage, and this library deliberately has none.

A line per call

The runtime emits one INFO record per call, whose message is the rendered line:
enable_call_log.py
Sizes, never values. The arguments and the response body appear only on the DEBUG path, which a host has to ask for explicitly. That third line is the one worth staring at: the model produced a call that did not satisfy the schema, so it was rejected here, in your process, before anything was sent. No request, no rate-limit budget, no charge.

A summary at the end of a run

CallCollector is a sink that keeps the records; format_call_summary renders them:
call_summary.py
The times are sums, not wall clock — concurrent calls are each counted once, so the totals exceed elapsed time.

Forwarding to something that keeps history

on_call takes any callable — the CallSink protocol. ToolCall.to_dict() uses OpenTelemetry’s attribute names where a convention exists, so forwarding needs no translation table:
otel_sink.py
A sink is called synchronously, once per invocation, after the call finishes and before any exception propagates. One that raises is caught and logged at DEBUG: a broken sink must never turn a working tool call into a failing one.

What is measured, and why that decomposition

Outcome, not just status code

Two of these have no equivalent anywhere else in a normal stack. The three reached_network: false outcomes are calls that cost nothing at the API — a number no HTTP-level metric can produce, because nothing HTTP happened. envelope_error is the opposite surprise: an API that answers 200 and puts the failure in the body, which every generic HTTP metric records as a success. See envelopes. The split between credential_unavailable and credential_rejected is drawn on reached_network, not on the status code the exception carries. A failed OAuth refresh raises a CredentialError carrying a 400 from the authorization server; filing that as “rejected” would report that the API turned you away when the API was never contacted. (One nuance worth knowing: a failed refresh did make a network call, just not to the tool’s API. reached_network is about the tool’s own request.) Validation is why the record is minted in Tool.ainvoke rather than in the executor: validate_input runs before the executor is reached, so a record minted any deeper would miss every rejected call.

Timings, split by owner

These fields exist to be acted on. Optimization is the other side of this page: which lever moves which number. A single duration would conflate four things with different owners; a slow credential provider and a slow API are the same number to anyone who only measured the total. overhead_ms is the one that answers is this layer in my way? — and a boundary layer should be able to answer that from the user’s own logs, on demand.

Sizes on both sides

saved_bytes and saved_ratio are derived. A negative saving is not hidden: a response handler that adds more than it removes is worth seeing. Three things this deliberately does not do:
  • It does not report tokens. Tokens need a tokeniser this library does not have, and a bytes-per-token constant would turn a measurement into a guess with a decimal point in it.
  • It does not claim bandwidth saved. payload_bytes is read after decompression. The currency here is the model’s context window, not egress.
  • It does not re-serialise the payload to make the ratio prettier. payload_bytes is the response as the server formatted it and context_bytes is compact JSON, so against an API that pretty-prints, part of the difference is that API’s whitespace.

Cost

Timings are a handful of perf_counter reads and are always taken. Sizes cost a serialisation of the arguments and of the result, so they are only computed when something is listening — a sink is attached, or the charter logger is enabled for INFO. With neither, the size fields are None and nothing extra is paid.

Where this stops

Everything above is derivable from one process’s own run. The moment a number needs yesterday — a baseline, “p95 doubled”, “this response grew three fields” — it needs storage, and storing your traffic is not something a library that promises it cannot see your data gets to do. That line is the same one drawn in the package docstring, and it is structural rather than commercial.