Normalization rules
Beacon normalizes OTLP attributes and hook payloads into the same event contract. The collector exporter looks for common runtime fields, while hook telemetry builds those fields directly from hook payloads.Model names
model is canonicalized at the point of writing, on both capture paths, the same way harness.name is. The value is trimmed and lowercased, and a provider prefix is removed: Anthropic/Claude-Sonnet-4-5, anthropic/claude-sonnet-4-5 and claude-sonnet-4-5 all record as claude-sonnet-4-5.
This matters because every token report groups by this field. Before canonicalization, one model reached through two runtimes — an OTLP runtime reporting the bare id, a plugin runtime reporting a provider-qualified one — produced two rows in the BY MODEL rollup, with a reader having no way to tell that from two genuinely different models.
The prefix is not discarded. It is recorded as gen_ai.provider.name, so a provider stays queryable in its own field rather than packed into a model string. A provider the runtime reported itself always wins: a prefix parsed out of a model name is only used when the runtime named no provider of its own.
Model ids themselves are never rewritten. Date suffixes, dots and vendor spellings are preserved as the runtime reported them, because a rule that folds claude-sonnet-4.6 into claude-sonnet-4-6 also turns gpt-4.1 into gpt-4-1, which is not a model under any name. A split row is visible to a reader; an invented id is not.
Context size is not usage
gen_ai.usage is additive: every report sums it to answer what a session spent. gen_ai.context is a level at one moment — how full the model’s window was on a call — and summing it answers nothing.
Qwen Code is the runtime that forced the split. Its Stop hook reports the prompt token count for a turn, which in a multi-turn session already contains every prior turn; added up, a session’s total grows with roughly the square of its length. Read as context occupancy the same number is exact, so it is recorded in a block nothing sums:
Both halves are required before either is recorded.
input_tokens is also the ecosystem’s usual name for a genuine additive usage count, so a reported limit beside it is what distinguishes a payload describing window occupancy from one reporting spend. Without that pairing the rule would reclassify other runtimes’ spend as context.
A reported limit_tokens also takes precedence over Asymptote’s static context-window table when computing utilization, because it reflects the tier the call actually ran under rather than the default for a model name.
Source mapping
OpenCode plugin mapping
OpenCode’s managed plugin mapschat.message to prompt.submitted;
tool.execute.before to tool.invoked; and successful terminal tool events to
command.executed, file.read, file.modified, mcp.tool_invoked, or
tool.completed according to tool semantics. Terminal tool errors become
tool.failed.
Completed assistant message metadata supplies model, finish reason,
runtime-reported cost, and canonical gen_ai.usage. Completed text and reasoning
parts use gen_ai.output.messages; reasoning additionally uses
agent.reasoning. Permission replies map once and always to
approval.allowed, reject to approval.denied, and unknown states to
approval.requested.
Structured session.diff arrays emit file events only for entries containing a
real path and change. Empty/cumulative duplicate diffs do not create pathless
file.modified records.
Cline plugin mapping
Cline’s managed plugin maps run start tosession.started, and to
prompt.submitted when the payload carries the message that started the task;
beforeTool to tool.invoked; afterTool to command.executed, file.read,
file.modified, mcp.tool_invoked, or tool.completed according to tool
semantics, and to tool.failed when the payload reports an error; run end to
session.ended; task cancels to session.ended at info severity with
session.cancel_reason set, since a cancel is an end rather than a failure; and
task errors to session.error.
Run end is read for its outcome rather than assumed to be a success. The plugin
surface has a single run-completion handler for every outcome and reports the
difference as a status on the context, so an aborted run normalizes to the cancel
shape and a failed one to session.error, exactly as the separately named
file-based hooks do. A run that states no outcome, or states one Beacon does not
recognize, stays a completion: mislabelling a finished task as failed is worse
than missing a label.
The same mapping accepts Cline’s file-based hook names (TaskStart,
UserPromptSubmit, PreToolUse, PostToolUse, TaskComplete, TaskCancel)
alongside its plugin handler names, so both of Cline’s hook surfaces normalize
identically. Stage names are matched on letters and digits only, so
tool_call_before, beforeTool, and PreToolUse all resolve to the same
lifecycle point. Lifecycle points Beacon does not map emit no event rather than a
generic record.
A file action whose payload carries no agent-derived path is recorded as
tool.completed rather than a pathless file.read or file.modified. Cline
addresses files relative to the workspace root, so paths are resolved against
that root before being written; an already-absolute path is untouched, and a
relative path with no resolvable root stays relative.
Canonical gen_ai.usage comes from the run-end payload only, including cache
reads and runtime-reported cost. Recording usage at a per-model-call boundary as
well would double-count every task’s tokens in usage rollups.
Event identity
Every event carries two identity fields, and both are filled on the hook path and the OTLP path alike.gen_ai.tool.call.id is the runtime’s own name for one tool invocation, read from whichever
attribute that runtime uses: Claude Code writes tool_use_id, Codex and OpenCode write call_id,
Cline writes callId, and Qwen Code writes tool_use_id or tool_call_id. It is what links a tool call to its result, an approval to the execution it
approved, and a hook event to the OTLP event describing the same action. Where two events share
one, they describe one call, and the writer records that call once rather than twice.
event.id is Beacon’s own identity for the event: a deterministic UUID, so the same event is
named the same way in any process and any release. Where the runtime named the action itself, the
UUID is derived from the session, action, target, and call ID, and nothing else, so the hook’s
report and the collector’s report of one action carry one event.id even though they are written
seconds apart with different fields. Otherwise it is derived from the event’s own content, which
makes re-reading a log produce the IDs it already had.
OpenTelemetry GenAI fields
Beacon preserves OpenTelemetry GenAI semantic convention fields under the nestedgen_ai object and also projects selected fields into Beacon’s common investigation fields. This keeps the raw GenAI context available without forcing downstream rules to parse runtime-specific attribute names.
Request and response metadata
Usage metadata
Usage also arrives as metric datapoints, not just span and log attributes. Claude Code’s
claude_code.token.usage and claude_code.cost.usage are expanded per datapoint. Codex’s live source is its completed turn span, whose input, cache-read, cache-write, output, and reasoning attributes map into the same gen_ai.usage fields with thread.id promoted to session.id. Legacy codex.turn.token_usage metric rows remain supported for older logs. gen_ai.usage.cost_usd carries runtime-reported cost only and is never derived from a local pricing table. See How token attribution works for how these normalized fields aggregate into local reports.
Prompt and content handling
Prompt text can come fromgen_ai.prompt, prompt, user_prompt, input.prompt, copilot_chat.user_request, or the first meaningful text value in gen_ai.input.messages. Empty input message arrays do not create prompt events by themselves.
Beacon accepts empty content.retention values for compatibility with older records. Truncation can be represented by top-level field_truncated and, for event-specific content state, content.truncated.
Category inference
When a runtime omitsevent.category, Beacon infers it from event.action:
Action inference and fidelity
Actions are resolved in order, and where the answer comes from is recorded on the event asevent.fidelity:
The distinction is structural rather than semantic: it records whether the source named the
operation or Beacon pattern-matched prose, not whether a given mapping is ideal. An action resolved
from a runtime’s own event name overrides an earlier pattern match, and the fidelity is raised with
it, so a runtime that names its events well is not penalized for also carrying a prose body.
Hook and plugin telemetry is
observed by construction, because a hook fires for a named lifecycle
event. The exception is approval synthesis: on runtimes that expose a pre-tool notification but no
approval hook, Beacon emits an approval.allowed event carrying approval.required so
approval-shaped detections have something to match. Nothing was gated there, so those events are
inferred.
Related
Unified telemetry schema
Return to the schema overview.
Schema fields
Review entities, optional context, and shared top-level fields.
Open Source Architecture
See where normalization fits in the local telemetry architecture.

