Telemetry and observers¶
The telemetry contract is typed in-process events delivered to registered observers. OpenTelemetry is an optional bridge over that contract, not the contract itself.
Nothing is written anywhere by default. A deployment that wants zero telemetry pays nothing, including no dependency.
Subscribing¶
class Recorder:
def on_event(self, event: ai.TelemetryEvent) -> None:
match event:
case ai.FirstToken(at_ms=ms):
metrics.ttft.observe(ms)
case ai.RequestCompleted(usage=usage, timing=timing):
metrics.tokens.inc(usage.output_tokens or 0)
case ai.RequestFailed(error=error):
log.warning("request failed: %s", error.detail)
client = ai.Client(providers, observers=[Recorder()])
# or later:
client.subscribe(Recorder())
Observers are synchronous and dispatched inline, so keep on_event fast — queue anything
slow. An observer that raises is isolated: the exception is swallowed and warned about once,
because a broken telemetry sink must never fail a generation.
The events¶
Request lifecycle: RequestStarted · TargetResolved · AttemptStarted · FirstToken
· AttemptCompleted · RetryScheduled · FallbackTriggered · RepairAttempted ·
RequestCompleted · RequestFailed
Degradation — the ones that make silent failures visible:
| Event | Emitted when |
|---|---|
ParameterDropped |
A provider accepts a parameter and discards it. |
UsageEstimated |
A usage figure was derived rather than reported. |
Anything AnyInfer drops or estimates is observable. That is a deliberate inversion of the
common drop_params=True-style design, where the whole point is that you do not find out.
Context reduction: ContextReduced — counts and ceilings only, never content.
Local subsystem: ServerLifecycle · DownloadProgress
Payloads are off by default¶
Prompt and response text are None unless an observer explicitly opts in:
client.subscribe(audit_log, payloads=True) # sees prompt and response text
client.subscribe(metrics) # never does
Stripping happens per observer, so one payload-consuming sink does not leak text to the others. Everything still passes redaction first.
Correlating events¶
Every request-lifecycle event carries a request_id. A fallback chain that tried three
targets emits one RequestStarted and one terminal event with the same id, so a trace reads
as a single request rather than three disconnected ones.
The OpenTelemetry bridge¶
from anyinfer import otel
otel.install(client) # spans and metrics, payload-free
Requires the [otel] extra; nothing OTel-related is imported otherwise. What the bridge
emits — one span per request, attempts as span events, GenAI semantic-convention metrics —
is covered in the observability guide.
The bridge is one consumer of the event contract. Applications that want structured data in-process — a JSONL trail, a SQLite evidence table — consume the events directly rather than round-tripping through spans.
Key takeaways
- Telemetry is typed in-process events to registered observers; OpenTelemetry is an optional bridge, not the contract itself.
- Prompt and response payloads are off by default, per observer, so one consumer's opt-in never leaks text to another.
- Degradation is observable by design —
ParameterDroppedandUsageEstimatedexist so silent failures in comparable gateways can't happen here.