Skip to main content

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 maps chat.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 to session.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 nested gen_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 from gen_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 omits event.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 as event.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.

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.