Telemetry and Redaction¶
Typed in-process events, payload-free by default, plus the redaction registry that keeps secrets out of everything. Concepts: telemetry · credentials and redaction.
Observing¶
anyinfer.Observer ¶
Bases: Protocol
A telemetry sink.
Implementations receive every event the client emits. Keep on_event fast and
non-blocking; queue work elsewhere if it might be slow.
anyinfer.TelemetryEvent
module-attribute
¶
TelemetryEvent = (
RequestStarted
| ArenaCompleted
| TargetResolved
| AttemptStarted
| FirstToken
| AttemptCompleted
| RetryScheduled
| FallbackTriggered
| RepairAttempted
| RequestCompleted
| RequestFailed
| ParameterDropped
| UsageEstimated
| ServerLifecycle
| DownloadProgress
| ContextReduced
| ProviderDiagnostic
| CachePlanned
| RateLimitWaited
| RateLimitObserved
| CredentialRotated
| BatchSubmitted
| BatchCompleted
)
Any event an observer may receive.
Ready-Made Sinks¶
Two sinks for the common cases, so a structured record does not have to be hand-written. Both are content-free unless the subscription opts into payloads, and every string they emit is redacted. For spans and metrics a backend can aggregate, use the OpenTelemetry bridge below instead.
anyinfer.LoggingObserver ¶
LoggingObserver(
logger: Logger | str | None = None,
*,
level: int | str = logging.INFO,
)
Emit each event as a structured record on a stdlib logging.Logger.
The message is the event name; the full mapping is attached as an anyinfer_event
record attribute, so a JSON log formatter can render it while a plain formatter still
prints something readable.
Both arguments accept the string forms a configuration file can express, because
observers blocks in anyinfer.toml reach this constructor through
build_observers and can only carry JSON/TOML scalars. A bad level is rejected here,
at construction, so build_observers' promise that a typo fails at load rather than
at the first event holds for the option values as well as the observer name — an
unresolvable level would otherwise raise inside isEnabledFor on every single event,
which the dispatcher suppresses after one warning, leaving a silently empty log.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logger
|
Logger | str | None
|
Where to log, as a |
None
|
level
|
int | str
|
Level to log every event at, as an int or a level name such as |
INFO
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Example
import logging logging.basicConfig(level=logging.INFO) observer = LoggingObserver() client.subscribe(observer) # doctest: +SKIP
anyinfer.JsonlObserver ¶
JsonlObserver(path: str | Path, *, flush: bool = True)
Append each event to a file as one JSON object per line.
Opened once and held open, because reopening per event turns a telemetry sink into the slowest thing on the request path. Writes are locked: the sync facade dispatches from its background loop thread while the caller's own thread may be subscribing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
File to append to. Parent directories are created. Created at mode 0600 on
POSIX — even payload-free telemetry names targets, models, and spend. Windows
has no equivalent through |
required |
flush
|
bool
|
Flush after every line. The default ( |
True
|
Example
with JsonlObserver("telemetry.jsonl") as observer: # doctest: +SKIP ... client.subscribe(observer)
on_event ¶
on_event(event: TelemetryEvent) -> None
Append one event. Never raises for a serialization problem.
anyinfer.events.sinks.event_to_dict ¶
event_to_dict(event: TelemetryEvent) -> dict[str, Any]
Render one event as a JSON-safe mapping, redacted.
The event's type name lands under event; every field keeps its own name. Shared by
both sinks, and useful on its own for a caller writing a third one.
Request Lifecycle Events¶
anyinfer.RequestStarted
dataclass
¶
RequestStarted(
request_id: str,
targets: tuple[Target, ...],
metadata: Mapping[str, str] = dict(),
prompt_text: str | None = None,
operation: InferenceOperation = "generation",
)
An inference request entered the router.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
targets |
tuple[Target, ...]
|
The fallback chain as requested, in the order the router will try it. |
metadata |
Mapping[str, str]
|
Caller-supplied labels from the request, passed through untouched. |
prompt_text |
str | None
|
The prompt text; |
operation |
InferenceOperation
|
Which inference operation this request is. Defaults to
|
anyinfer.TargetResolved
dataclass
¶
TargetResolved(request_id: str, target: ResolvedTarget)
A target string resolved to a concrete provider and model.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The concrete provider and model the target string resolved to. |
anyinfer.AttemptStarted
dataclass
¶
AttemptStarted(
request_id: str,
target: ResolvedTarget,
attempt_number: int,
)
An attempt against one resolved target began.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target being attempted. |
attempt_number |
int
|
1-based count of attempts against this target; retries increment it, and falling back to a new target restarts it at 1. |
anyinfer.FirstToken
dataclass
¶
FirstToken(
request_id: str, target: ResolvedTarget, at_ms: float
)
The first content delta arrived — the centrally-measured TTFT.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target that produced the token. |
at_ms |
float
|
Milliseconds from attempt start to the first content delta. |
anyinfer.AttemptCompleted
dataclass
¶
AttemptCompleted(
request_id: str,
target: ResolvedTarget,
usage: Usage,
timing: Timing,
finish_reason: str,
)
An attempt finished successfully.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target that served the attempt. |
usage |
Usage
|
Token usage for the attempt, merged across every report the provider sent. |
timing |
Timing
|
Centrally-measured attempt timings, comparable across providers. |
finish_reason |
str
|
Normalized reason generation stopped, e.g. |
anyinfer.RetryScheduled
dataclass
¶
RetryScheduled(
request_id: str,
target: ResolvedTarget,
attempt_number: int,
delay_s: float,
error: ErrorInfo,
)
A retryable failure will be retried against the same target after a delay.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target that will be retried. |
attempt_number |
int
|
The 1-based attempt that just failed; the retry is the next one. |
delay_s |
float
|
Seconds the router sleeps before the retry. |
error |
ErrorInfo
|
Snapshot of the retryable failure. |
anyinfer.FallbackTriggered
dataclass
¶
FallbackTriggered(
request_id: str,
from_target: ResolvedTarget,
to_target: Target,
error: ErrorInfo | None = None,
)
A target was abandoned; the router advanced to the next in the chain.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
from_target |
ResolvedTarget
|
The resolved target that was abandoned. |
to_target |
Target
|
The next target string the router will try, not yet resolved. |
error |
ErrorInfo | None
|
Snapshot of the failure that caused the abandonment, or |
anyinfer.RepairAttempted
dataclass
¶
RepairAttempted(
request_id: str,
target: ResolvedTarget,
attempt_number: int,
mechanism: Mechanism | None,
errors: tuple[str, ...] = (),
raw_text: str | None = None,
)
A schema violation triggered a repair re-prompt.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target being re-prompted. |
attempt_number |
int
|
1-based count of repair attempts within this generation. |
mechanism |
Mechanism | None
|
The structured-output mechanism in force when validation failed, when one was chosen. |
errors |
tuple[str, ...]
|
The schema-validation messages that triggered the repair. |
raw_text |
str | None
|
The response text that failed validation; |
anyinfer.ParameterDropped
dataclass
¶
ParameterDropped(
request_id: str,
target: ResolvedTarget,
parameter: str,
reason: str,
)
A requested parameter was not honored as asked, because the target cannot.
Dropping a parameter silently is how a caller ends up debugging why temperature=0
had no effect. Every drop is observable instead. The same applies to a parameter
honored only in part — a repair budget clamped to a provider's ceiling is reported
here too, since a budget quietly reduced from three to one is no more discoverable
than one ignored outright.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target the parameter was withheld from. |
parameter |
str
|
Name of the request parameter that was not honored, dotted for a field
of a compound one ( |
reason |
str
|
Human-readable explanation of what the target did instead. |
anyinfer.ProviderDiagnostic
dataclass
¶
ProviderDiagnostic(
target: ResolvedTarget | None,
diagnostic: Diagnostic,
request_id: str | None = None,
)
A provider reported something about its own runtime.
Emitted after an attempt for providers that declare reports_diagnostics, and
whenever diagnostics() is called directly. The same text also lands on
Generation.warnings, so a caller
reading only results still sees it; this event is for observers that want it
correlated with the request that hit it.
Attributes:
| Name | Type | Description |
|---|---|---|
target |
ResolvedTarget | None
|
The resolved target whose runtime is being described, or |
diagnostic |
Diagnostic
|
What the provider reported. |
request_id |
str | None
|
Correlation id, or |
anyinfer.ContextReduced
dataclass
¶
ContextReduced(
strategy: str,
representation: str,
candidate_count: int,
selected_count: int,
omitted_count: int,
estimated_tokens: int,
max_tokens: int,
binding_constraints: tuple[str, ...] = (),
calls: int = 0,
)
Context was reduced to fit a budget.
Emitted for a reduced document corpus and for a compacted message history alike — both emulate a larger context window, and emulation is observable rather than silent. Content-free by construction: counts and ceilings only, never paths, document text, or message text — a path name can itself be sensitive.
Attributes:
| Name | Type | Description |
|---|---|---|
strategy |
str
|
The strategy requested ( |
representation |
str
|
The strategy actually applied. |
candidate_count |
int
|
Documents, or messages — offered to the reducer. |
selected_count |
int
|
Documents represented at detail fidelity, or messages kept. |
omitted_count |
int
|
Documents not represented in detail, or messages dropped. |
estimated_tokens |
int
|
Planning-side estimate of the rendered envelope, or of the compacted conversation. |
max_tokens |
int
|
The budget the reduction was held to. |
binding_constraints |
tuple[str, ...]
|
Which ceilings excluded at least one document. |
calls |
int
|
Generation calls spent; non-zero only for |
anyinfer.ArenaCompleted
dataclass
¶
ArenaCompleted(
request_id: str,
target_count: int,
strategy: str,
agreement: int | None,
calls: int,
memoized_tool_calls: int,
synthesized: bool,
)
A bounded multi-target arena finished, without carrying answer content.
anyinfer.CachePlanned
dataclass
¶
CachePlanned(
request_id: str,
target: ResolvedTarget,
mechanism: str,
mark_count: int,
estimated_cacheable_tokens: int,
)
The core decided how to engage a target's prompt cache.
Emitted only when a policy was in force and the target offered a mechanism. A policy
that found nothing to use reports a ParameterDropped instead, because the interesting
fact there is the degradation, not the plan.
Content-free: counts and mechanism only. What was marked is a position, never text.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target the plan applies to. |
mechanism |
str
|
|
mark_count |
int
|
How many marks were placed; always zero for |
estimated_cacheable_tokens |
int
|
Planning-side size of what the plan tries to cache. An intention, not a saving — realized savings come only from the provider's own reported usage. |
anyinfer.RateLimitWaited
dataclass
¶
RateLimitWaited(
request_id: str,
provider_id: str,
waited_s: float,
reason: Literal[
"concurrency", "interval", "provider-headers"
],
target: ResolvedTarget | None = None,
)
A request was held back by client-side pacing before it was dispatched.
Emitted so a paced request never looks like a slow provider. The same wait also lands
in the attempt's timing.phases["queued_ms"], because latency a caller cannot
attribute is a support ticket.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id of the waiting request; empty when the wait happened outside a tracked generation, as on a model listing. |
provider_id |
str
|
The provider instance whose limiter did the waiting. |
waited_s |
float
|
How long the request was held. |
reason |
Literal['concurrency', 'interval', 'provider-headers']
|
|
target |
ResolvedTarget | None
|
The resolved target, when the wait belongs to a generation attempt. |
anyinfer.RateLimitObserved
dataclass
¶
RateLimitObserved(
provider_id: str,
requests_remaining: int | None = None,
tokens_remaining: int | None = None,
resets_in_s: float | None = None,
)
A provider reported its rate-limit state on a response.
Emitted at most once per response, and only when the provider declared a header dialect
and actually populated it. Purely what the provider said — this library adds no estimate
of its own, and a field the provider left out stays None rather than becoming a
guess.
Attributes:
| Name | Type | Description |
|---|---|---|
provider_id |
str
|
The provider instance that reported. |
requests_remaining |
int | None
|
Requests left in the current window, when stated. |
tokens_remaining |
int | None
|
Tokens left in the current window, when stated. |
resets_in_s |
float | None
|
Seconds until the window resets, when stated in a form that can be read as a duration. |
anyinfer.UsageEstimated
dataclass
¶
UsageEstimated(
request_id: str,
target: ResolvedTarget,
field_name: str,
method: str,
)
A usage figure was derived rather than reported by the provider.
Estimated and reported numbers must never be indistinguishable downstream; this event is what marks the difference for observers.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target the estimate applies to. |
field_name |
str
|
The usage field that was estimated, e.g. |
method |
str
|
How the estimate was derived. |
anyinfer.RequestCompleted
dataclass
¶
RequestCompleted(
request_id: str,
target: ResolvedTarget,
usage: Usage,
timing: Timing,
repair_attempts: int = 0,
response_text: str | None = None,
)
A request produced a result.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
target |
ResolvedTarget
|
The resolved target that produced the result. |
usage |
Usage
|
Final token usage for the request. |
timing |
Timing
|
Final centrally-measured timings for the request. |
repair_attempts |
int
|
How many repair re-prompts were needed; |
response_text |
str | None
|
The final response text; |
anyinfer.RequestFailed
dataclass
¶
RequestFailed(request_id: str, error: ErrorInfo)
A request exhausted its route without a result.
Attributes:
| Name | Type | Description |
|---|---|---|
request_id |
str
|
Correlation id shared by every event this request emits. |
error |
ErrorInfo
|
Snapshot of the terminal failure, after every target and retry was spent. |
Local Subsystem Events¶
anyinfer.DownloadProgress
dataclass
¶
DownloadProgress(
artifact_id: str,
downloaded_bytes: int,
total_bytes: int | None,
done: bool = False,
phase: str = "",
file_index: int = 0,
file_count: int = 0,
filename: str = "",
session_bytes: int = 0,
)
Progress of a model acquisition.
downloaded_bytes and total_bytes are aggregate across the whole acquisition,
which is what their names have always implied. Earlier builds reported them per file,
so a sharded artifact restarted the counter at zero on every shard with no way for an
observer to tell. The remaining fields carry the per-file detail that the aggregate
figures deliberately no longer mix in.
Attributes:
| Name | Type | Description |
|---|---|---|
artifact_id |
str
|
The artifact or catalog variant being acquired. |
downloaded_bytes |
int
|
Bytes present across every file, including bytes that were already on disk before this run, so resuming reports the resumed position. |
total_bytes |
int | None
|
Total expected bytes, or |
done |
bool
|
Whether the acquisition finished. |
phase |
str
|
Which stage emitted this, when the acquisition engine supplied one. |
file_index |
int
|
1-based index of the file that most recently advanced. |
file_count |
int
|
How many files this acquisition covers. |
filename |
str
|
Name of that file. |
session_bytes |
int
|
Bytes this run actually transferred, as opposed to resumed. |
anyinfer.ServerLifecycle
dataclass
¶
ServerLifecycle(
server_id: str,
state: Literal[
"starting",
"ready",
"stopping",
"stopped",
"crashed",
],
detail: str = "",
)
A supervised local server changed state.
Attributes:
| Name | Type | Description |
|---|---|---|
server_id |
str
|
Identifies the supervised server — the key of the model it serves. |
state |
Literal['starting', 'ready', 'stopping', 'stopped', 'crashed']
|
The state the server just entered. |
detail |
str
|
Human-readable context, such as a stop reason or a tail of crash output; empty when there is nothing to add. |
anyinfer.CredentialRotated
dataclass
¶
CredentialRotated(
provider: str,
trigger: Literal["ttl", "auth-failure"] = "ttl",
)
A provider instance's credential changed and its adapter was rebuilt.
Emitted only when the resolved value actually moved — a TTL expiring and the credential re-resolving to the same secret is not an event, because nothing happened. Rotation discards a connection pool mid-flight, so an operator watching latency deserves to know which second it was.
Payload-free by construction: the credential is never carried here, and neither is the change-detection digest.
Attributes:
| Name | Type | Description |
|---|---|---|
provider |
str
|
The provider instance whose adapter was rebuilt. |
trigger |
Literal['ttl', 'auth-failure']
|
What prompted the re-resolution — |
anyinfer.BatchSubmitted
dataclass
¶
BatchSubmitted(
batch_id: str, target: ResolvedTarget, line_count: int
)
A deferred batch was accepted by a provider.
Payload-free: the line count is a number, and the batch id is the provider's own handle rather than anything derived from the requests. Nothing about what was asked is carried, which is the same rule every other event here follows.
Attributes:
| Name | Type | Description |
|---|---|---|
batch_id |
str
|
The provider's id for the job. |
target |
ResolvedTarget
|
Where it was submitted. |
line_count |
int
|
How many requests went in. |
anyinfer.BatchCompleted
dataclass
¶
BatchCompleted(
batch_id: str,
target: ResolvedTarget,
status: str,
completed: int,
failed: int,
)
A finished batch was collected.
Emitted on collection, not completion — nothing here polls, and a batch that finished overnight becomes observable the moment a caller asks for it.
Attributes:
| Name | Type | Description |
|---|---|---|
batch_id |
str
|
The provider's id for the job. |
target |
ResolvedTarget
|
Where it ran. |
status |
str
|
The batch's terminal status. |
completed |
int
|
Lines that produced a result. |
failed |
int
|
Lines that did not. |
Redaction¶
anyinfer.RedactionRegistry ¶
RedactionRegistry()
A thread-safe set of secrets to strip from outbound strings.
register ¶
register(secret: str | None) -> None
Register a secret for redaction, along with its common encodings.
Redaction is exact-substring matching, so a secret that reaches a string in an encoded form — JSON-escaped inside a serialized body, percent-encoded in a URL, base64'd in an auth header — would not match the raw value and would survive. Each such form is registered alongside the original, which is cheap (a handful of extra strings per credential) and closes the gap for the encodings that actually occur on the request path.
This is defense in depth, not a guarantee: an encoding nobody anticipated still slips through, which is exactly why the cassette audit exists as a second net.
Values shorter than MIN_SECRET_LEN are ignored: redacting them would corrupt
unrelated text far more often than it would protect anything. The same floor is
applied to each derived form.
__len__ ¶
__len__() -> int
Number of registered secrets — not the number of strings matched against.
One registration also stores that secret's encoded forms; counting those would make this number an implementation detail of the encoding list.
anyinfer.register_secret ¶
register_secret(secret: str | None) -> None
Register a secret with the process-wide registry.
anyinfer.redact ¶
redact(text: str) -> str
Redact registered secrets from text using the process-wide registry.
OpenTelemetry Export¶
The optional [otel] extra maps these events onto OpenTelemetry spans and metrics.
Guide: observability.
anyinfer.otel.install ¶
install(
client: Any, *, record_payloads: bool = False
) -> OTelObserver
Attach an OTelObserver to a client.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
client
|
Any
|
An |
required |
record_payloads
|
bool
|
Attach prompt and response text to spans. |
False
|
Returns:
| Type | Description |
|---|---|
OTelObserver
|
The observer, so it can be detached later. |
anyinfer.otel.OTelObserver ¶
OTelObserver(
*,
tracer: Any = None,
meter: Any = None,
record_payloads: bool = False,
)
Maps AnyInfer telemetry events onto OpenTelemetry spans and metrics.
One span per request, with attempts as span events. Requests and attempts are
correlated by request_id, so a fallback chain reads as a single trace rather than
several disconnected ones.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tracer
|
Any
|
An OpenTelemetry tracer. Defaults to one from the global provider. |
None
|
meter
|
Any
|
An OpenTelemetry meter. Defaults to one from the global provider. |
None
|
record_payloads
|
bool
|
Attach prompt and response text to spans. Off by default, matching
the payload-free default of the event contract itself. Subscribe with
|
False
|
Raises:
| Type | Description |
|---|---|
ConfigError
|
If |
on_event ¶
on_event(event: TelemetryEvent) -> None
Handle one telemetry event.
Never raises: the dispatcher isolates observer failures, but a telemetry bridge that can break a generation would be a poor trade regardless.
anyinfer.otel.GEN_AI
module-attribute
¶
GEN_AI = 'gen_ai'
Prefix of the GenAI semantic-convention attribute namespace.