model

package
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 3 Imported by: 0

Documentation

Overview

Package model holds the core domain types shared by adapters, storage, the collector and reporting. It depends on nothing else in the project.

The core types model the ledger's shape: UsageEvent is one immutable observed record (deduplicated on DedupKey, stored append-only); AggregateSnapshot is one observation of a source's cumulative counters, materialized by the collector into synthetic usage events via monotonic-with-reset deltas; SourceCheckpoint is the incremental-collection state for a source (per-source seam so a source can skip unchanged reads).

SourceClass partitions adapters by how they expose usage: EventLevel sources emit discrete records (one per request/message) and deduplicate cleanly; Aggregate sources expose only running counters, snapshots of which the collector turns into synthetic delta events.

Dimensioning (Tool, Provider, ServiceTier) marks each usage record for categorization and pricing. Tool is the agent CLI (ToolClaudeCode, ToolCopilot, etc.). Provider is the billing vendor whose rate card applies (ProviderAnthropic, etc.), taken from source data when named or stamped for the vendor an adapter always talks to. ServiceTier names the requested priority or batch mode ("standard", "batch", etc.). ReasoningMode describes how a tool's reasoning tokens relate to output tokens (subset: already contained; additive: billed on top), and the pricing engine reads it to avoid double-charging.

Token counting: Adapters normalize provider counts into disjoint buckets (InputTokens, OutputTokens, CacheCreationTokens, CacheReadTokens, ReasoningTokens), and must set TotalTokens as the provider's own authorized total, which varies by vendor (cache tokens additive for Anthropic, subset of input for OpenAI). The collector compares both and the cost engine prices according to ReasoningMode and the long-context tier crossed by the whole prompt. Cost is stamped at collect time (CostMicroUSD) or left nil when unpriced; 0 never appears (unpriced and free are distinguishable).

Raw is the provider payload kept for audit, built by adapters from an explicit allow-list of usage/model/identity fields — never stripped from a whole record. It is an audit payload, not a backfill source (schema columns carry everything cost and reporting need), and it is not marshalled by default (export gates it behind --include-raw; history predates the allow-list and still holds whole transcript lines). config privacy.no_raw drops it entirely via the collector.

Index

Constants

View Source
const (
	ToolClaudeCode = "claude-code"
	ToolCodex      = "codex"
	ToolCopilot    = "copilot"
	ToolOpenCode   = "opencode"
	ToolHermes     = "hermes"
	ToolGemini     = "gemini"
	ToolAgy        = "agy"
	ToolPi         = "pi"
	ToolOpenClaw   = "openclaw"
	ToolCrush      = "crush"
	ToolKimiCode   = "kimi-code"
	ToolReasonix   = "reasonix"
	ToolDSH        = "dsh"
	ToolQwenCode   = "qwen-code"
	ToolGoose      = "goose"
	// ToolCline names the HARNESS, not the CLI surface the clinecli adapter
	// reads: a future adapter for the same product's editor extension shares
	// this id and mints its own, non-colliding dedup keys.
	ToolCline = "cline"
)

Tool identifiers — the "tool" categorisation dimension (which agent CLI).

ToolGemini is retired: Antigravity replaced the Gemini CLI, so no adapter collects it and nothing new is stamped with it. The identifier stays because usage_events is append-only and existing rows still carry it — deleting it would leave that history without a colour, a glyph or a reasoning mode.

View Source
const (
	ProviderAnthropic = "anthropic"
	ProviderOpenAI    = "openai"
	ProviderGoogle    = "google"
	ProviderGitHub    = "github"
)

Billing provider identities — the "provider" dimension of a priced event (whose price list applies to the request). Adapters whose source data names the provider pass it through verbatim (opencode's providerID, hermes' billing_provider); the rest stamp the constant for the vendor they always talk to. An empty provider means unknown and is rendered as such.

View Source
const UnpricedMark = "-"

UnpricedMark is what a cost renders when nothing behind it could be priced. Deliberately not "$0.00": a missing price is not a free request. ASCII so the value survives pipes, dumb terminals and CSV round trips.

Variables

This section is empty.

Functions

func FormatCost

func FormatCost(microUSD int64, approximate, known bool) string

FormatCost renders a micro-USD amount as display copy, and is the single definition of that copy for every surface — the report tables, the JSON summaries and the dashboard. It lives here because report and tui are peers in the layering and may not import each other, and two hand-kept copies of a money format drift.

known is false when nothing in the amount could be priced at all, which renders as UnpricedMark rather than a zero. approximate marks a figure that includes rows valued at display time, or that omits rows nothing could value: either way the number is not the bill, and it carries a leading "~" so it cannot be read as exact. Sub-cent amounts widen to four decimals, and anything smaller than that renders as "<$0.0001" — a real charge must never print as $0.00.

func VendorPriceSourceFamilies

func VendorPriceSourceFamilies() []string

VendorPriceSourceFamilies returns the adapter-stamped price-source families. A source belongs to family f when it equals f or begins with f+"-". The slice is a copy.

func VendorPriceSources

func VendorPriceSources() []string

VendorPriceSources returns the exact adapter-stamped price sources. The slice is a copy: the vocabulary is closed and callers must not extend it in place.

Types

type ActivityCapture

type ActivityCapture string

ActivityCapture says what a tool's adapter can tell you about its tool calls.

const (
	// ActivityExact: the adapter emits activity rows AND names the usage row
	// that paid for each call, so the call's share of the turn is derivable.
	ActivityExact ActivityCapture = "exact join"
	// ActivityUnattributed: the adapter emits activity rows but the source gives
	// it no handle onto the usage record, so every call's cost is UNKNOWN — not
	// zero. Codex's token_count records share no identity with its call records;
	// copilot's execute_tool span is a sibling of the chat span, not its parent;
	// goose's usage_ledger rows carry no message id.
	ActivityUnattributed ActivityCapture = "recorded, unattributed"
	// ActivityNone: the adapter emits no activity rows at all. Its surface
	// exposes usage and nothing else.
	ActivityNone ActivityCapture = "none"
)

type ActivityEvent

type ActivityEvent struct {
	// ID is the activity row id (activity_events.id), set by listing queries and
	// left zero on a record that has not been read back from the store.
	ID   int64        `json:"-"`
	Tool string       // agent CLI id (ToolClaudeCode, ...)
	Kind ActivityKind // tool | skill | hook
	// Name is the invoked tool/skill/hook name, verbatim ("Bash", "Read",
	// "mcp__plugin__search", "artifact-design", "stop_hook_summary").
	Name string

	SessionID string    // provider session id
	Project   string    // workspace / cwd path
	Model     string    // model id of the turn, when the source names one
	EventTime time.Time // when the call happened (from the source)
	// ObservedTime is when the daemon read/stored the record.
	ObservedTime time.Time

	// UsageDedupKey is the dedup_key of the usage_events row this call is
	// attributable to, or "" when the source gives no join (codex records its
	// function calls and its token counts in unrelated records; hooks carry no
	// usage at all). It is deliberately NOT a foreign key: a call is an observed
	// fact even when its usage row was skipped as a poison row or predates
	// activity collection, and the read path left-joins, so a missing partner
	// contributes no cost instead of losing the call.
	UsageDedupKey string
	MessageID     string // provider message id (if any)
	RequestID     string // provider request id (if any)
	// TurnSeq is this call's 0-based index among the calls of its turn, and
	// CallsInTurn is how many calls that turn contained (>= 1). Together they
	// are the denominator and the tie-break of the read-time cost split.
	TurnSeq     int
	CallsInTurn int

	SourcePath string // file/db the record came from
	DedupKey   string // globally-unique stable key; inserts conflict-skip on this
}

ActivityEvent is one immutable observed AGENT ACTIVITY record: a tool call, a skill invocation or a hook firing. It is a sibling of UsageEvent, not a part of it — activity is not token accounting, and mixing the two would put rows that cost nothing into the ledger that answers "what did this cost".

PRIVACY: this type is names and counts ONLY, by construction. There is no field for a tool's INPUT — no command string, no file path, no prompt, no leftover `.input` blob — and no raw payload of any kind, so `privacy.no_raw` has nothing to drop here. The one input field any adapter reads is the skill NAME of a Skill call, which is the fact being recorded rather than content. MCP tool names (mcp__server__tool) are names too and are kept verbatim: which server got called is the entire point.

COST ATTRIBUTION. An activity row stores NO token counts. It carries UsageDedupKey, the dedup key of the usage_events row whose provider record contained this call, and CallsInTurn, how many calls shared that one usage object. Cost and tokens are derived on READ by joining to the ledger and dividing by CallsInTurn (see store.SummarizeActivity).

That shape is the honest one. One assistant turn emits several tool_use blocks against a SINGLE usage object, so any per-call token count copied onto each row would multiply the turn's real cost by the number of calls in it. Storing the number once — in usage_events, where it already lives — and referencing it makes over-attribution structurally impossible rather than a rule someone has to keep remembering: the ledger stays the single source of cost truth, and the split is a documented read-time convention that can be changed without rewriting an append-only table.

type ActivityKind

type ActivityKind string

ActivityKind marks what sort of invocation an ActivityEvent records. The vocabulary is closed and enforced by a CHECK constraint on activity_events: a new kind is a schema change, not a string an adapter may invent.

const (
	// ActivityTool is one tool/function call the agent made (Bash, Read,
	// mcp__server__tool, codex's exec, opencode's bash, ...).
	ActivityTool ActivityKind = "tool"
	// ActivitySkill is a skill invocation. Claude Code models these as a tool
	// call named "Skill" whose input names the skill; the skill NAME is what
	// this row carries, so "which skill" is a group-by rather than a parse.
	ActivitySkill ActivityKind = "skill"
	// ActivityHook is a hook firing. It carries no usage of its own, so it
	// never attributes tokens.
	ActivityHook ActivityKind = "hook"
)

type AggregateSnapshot

type AggregateSnapshot struct {
	Tool  string
	Key   string
	Model string
	// Provider is the billing identity behind the session (ProviderAnthropic,
	// ...), carried onto the synthetic event like the other attributes.
	Provider     string
	SessionID    string
	Project      string
	ObservedTime time.Time

	InputTokens         int64
	OutputTokens        int64
	CacheCreationTokens int64
	CacheReadTokens     int64
	ReasoningTokens     int64
	TotalTokens         int64

	SourcePath string
	Raw        string
}

AggregateSnapshot is one observation of a source's cumulative/growing counters for a single accumulator cell. The collector compares the new snapshot against the last stored state for the same (Tool, Key) to derive a positive delta (monotonic-with-reset), which it materialises as one immutable usage event. Used by sources whose per-record totals GROW between polls:

  • hermes — per-session running totals; Key = session_id
  • gemini — per-turn cumulative snapshots; Key = sourcePath + "|" + turn id
  • agy — cumulative result.usage from captured print-mode stream JSON

Key is the accumulator identity (must be stable across polls and unique per growing cell). SessionID/Model/Project are the reportable attributes carried onto the synthetic event.

type CacheWriteTTL

type CacheWriteTTL struct {
	Ephemeral5m int64
	Ephemeral1h int64
}

CacheWriteTTL splits a cache-creation write by the lifetime requested for the entry. Anthropic prices a 1h cache write above the 5m write, so the pricing stamp needs the split even though the ledger stores only the combined CacheCreationTokens count. A zero split means the source reported none: the whole cache write is priced at the 5m rate.

type CodeChange

type CodeChange struct {
	Tool      string
	ChangeID  string
	SessionID string
	Project   string
	// Known distinguishes observed counts, including zero, from unavailable
	// data. Unknown snapshots have zero counts and replace older known values.
	Known        bool
	LinesAdded   int64
	LinesRemoved int64
	UpdatedAt    time.Time
	ObservedTime time.Time
}

CodeChange is the latest line-count snapshot a harness reports for one change identity, such as a user turn. It is not a token usage event or the final repository diff. A later snapshot may decrease after an undo. Counts and identity only: no patch text, file contents or tool arguments.

type CostProvenance

type CostProvenance string

CostProvenance names who produced a cost figure.

const (
	// CostAbsent: no cost at all. An empty price_source, which is what an
	// unpriced row carries. Never "free".
	CostAbsent CostProvenance = "none"
	// CostVendor: the harness's own accounting, stamped by its adapter and never
	// overwritten by the price ladder.
	CostVendor CostProvenance = "vendor-reported"
	// CostComputed: valued by this project from a public rate card — the LiteLLM
	// ladder, the embedded snapshot, or a configured override. An estimate.
	CostComputed CostProvenance = "computed"
)

func PriceProvenance

func PriceProvenance(priceSource string) CostProvenance

PriceProvenance classifies a stored price_source.

The default is CostComputed, and that direction is deliberate: the ladder's own rungs are open-ended ("litellm-<date>", "embedded-<date>", "override", their "+"-joined composites and the "+long-context" suffix), so enumerating them would mean chasing every new one, while the vendor set is closed and grows only when an adapter learns to read a price. An unrecognised source is therefore reported as an estimate — the reading that understates confidence rather than overstating it.

type EventKind

type EventKind string

EventKind marks a normal usage record vs an appended correction. History is never rewritten; corrections are appended as KindAdjustment rows.

const (
	KindUsage      EventKind = "usage"
	KindAdjustment EventKind = "adjustment"
)

type ReasoningMode

type ReasoningMode string

ReasoningMode describes how a tool's reported reasoning tokens relate to its output tokens. The pricing engine reads it to decide whether reasoning is already paid for by the output count or must be billed on top of it.

const (
	// ReasoningSubset: reasoning tokens are already contained in the reported
	// output tokens. Price output only; billing reasoning again double-charges.
	ReasoningSubset ReasoningMode = "subset"
	// ReasoningAdditive: reasoning tokens are reported alongside output and are
	// NOT part of it. Price output and reasoning, both at the output rate.
	ReasoningAdditive ReasoningMode = "additive"
)

func ReasoningModeFor

func ReasoningModeFor(tool string) ReasoningMode

ReasoningModeFor returns the reasoning billing mode for a tool id. An unknown tool falls back to ReasoningSubset — the conservative direction, which can under-bill but never charges the same token twice.

type ReasoningReport

type ReasoningReport string

ReasoningReport says how a tool's source reports reasoning tokens. It is the display face of reasoningModes plus the one state that map expresses by ABSENCE: a source with no reasoning counter at all.

const (
	// ReasoningReportSubset: reasoning is already inside the output count.
	ReasoningReportSubset ReasoningReport = "subset"
	// ReasoningReportAdditive: reasoning is reported beside output, not in it.
	ReasoningReportAdditive ReasoningReport = "additive"
	// ReasoningReportNone: the source carries no reasoning counter, so there is
	// no relationship to state. Distinct from "subset": subset is a claim about
	// a number that exists.
	ReasoningReportNone ReasoningReport = "not reported"
)

func ReasoningReportFor

func ReasoningReportFor(tool string) ReasoningReport

ReasoningReportFor says how a tool's source reports reasoning tokens. It reads reasoningModes rather than a second table: a tool present there reports the count with the stated relationship to output, and a tool ABSENT from it reports no count at all — which is why ReasoningModeFor's conservative subset fallback must not be used here. "Falls back to subset for pricing" and "reports a subset" are different statements, and only the first is true of crush, kimi-code, goose and cline.

type SourceCheckpoint

type SourceCheckpoint struct {
	Tool       string
	SourcePath string
	Size       int64  // file size in bytes at the last completed read
	MTimeNS    int64  // file mtime (unix nanoseconds) at the last completed read
	Offset     int64  // byte offset consumed (append-only JSONL tail reads)
	Watermark  int64  // max rowid consumed (database sources)
	State      string // adapter-specific JSON (baselines, manifests, gates)
}

SourceCheckpoint is mutable per-source incremental-collection state, keyed by (Tool, SourcePath). It lets an adapter skip or tail-read a source whose content is unchanged or append-only since the last cycle. It is NOT history: losing a checkpoint only costs a full re-read, never data (event dedup keys and aggregate_state make re-reads idempotent). The dangerous direction is a checkpoint that outruns its data — which is why the store persists it in the same transaction as the events it accounts for.

type SourceClass

type SourceClass string

SourceClass distinguishes how a source exposes usage data.

const (
	// EventLevel sources expose discrete, individually-identifiable usage
	// records (one per API request / message). They deduplicate cleanly via a
	// stable DedupKey and are immune to later file deletion once stored.
	EventLevel SourceClass = "event"
	// Aggregate sources expose only running/cumulative counters. The collector
	// snapshots them and materialises positive deltas as synthetic events using
	// a monotonic-with-reset accumulator (see PLAN.md).
	Aggregate SourceClass = "aggregate"
)

type ToolCapability

type ToolCapability struct {
	Tool      string
	Cost      CostProvenance
	Activity  ActivityCapture
	Reasoning ReasoningReport
	Tier      VerificationTier
}

ToolCapability is one tool's declaration.

func RetiredCapabilities

func RetiredCapabilities() []ToolCapability

RetiredCapabilities returns the declarations of every tool no adapter collects any more, each with its Reasoning filled. The slice is a copy.

The composition root merges these UNDER the registry's own declarations, so a tool that comes back to life is described by its adapter and not by this list.

type TurnContext

type TurnContext struct {
	// UsageDedupKey is the dedup_key of the usage_events row this context
	// describes. Together with Dimension it is the identity of the record: one
	// usage row, one value per dimension.
	UsageDedupKey string
	Tool          string // agent CLI id (ToolClaudeCode, ...)
	// Dimension names which axis this context is on. Must be one of the closed
	// vocabulary; the store's CHECK constraint refuses anything else.
	Dimension TurnDimension
	// Value is the name the turn ran under, verbatim ("workflow-subagent",
	// "adhd", "browser_eval", "ruflo", "mattpocock-skills"). Never empty — a
	// context with no value is not a context.
	Value string

	SessionID string    // provider session id
	Project   string    // workspace / cwd path
	Model     string    // model id of the turn
	EventTime time.Time // the usage event's own time, copied so both ledgers
	// place the turn in the same instant
	ObservedTime time.Time // when the daemon read/stored the record
	SourcePath   string    // file/db the record came from
}

TurnContext records that ONE usage event was produced while the agent was operating under ONE named thing along ONE dimension — inside a skill, as a subagent, serving an MCP tool, and so on. It answers "what did X cost", which the activity ledger alone cannot: an ActivityEvent of kind=skill records the turn that INVOKED a skill, not the thousands of turns the skill then went on to spend, and there is no activity row at all for "this turn ran as workflow-subagent".

TURN CONTEXT IS A PROPERTY OF THE TURN, NOT A CALL WITHIN IT. That single distinction is why this is a separate record type rather than another ActivityEvent kind, and it is worth stating precisely.

An ActivityEvent is one CALL. A turn emits several of them against a single usage object, so each takes a divided share (calls_in_turn) and the shares sum back to the turn. A turn context is not a call — it is the answer to "what was running when this turn happened", and along ANY ONE dimension a turn has AT MOST ONE. The source enforces that: every one of claude-code's five attribution fields is a scalar string on the record, verified over the whole local corpus (99,894 field occurrences across the five, 100% of them JSON strings, none an array or object), so a usage row cannot carry two agents or two skills. The store keys these rows by (UsageDedupKey, Dimension), which makes that a database constraint rather than an invariant a reader must trust.

Therefore the cost of a value along a dimension is the sum over DISTINCT usage rows whose context is that value, with NO division and no divisor to share. Over-attribution WITHIN a dimension is not prevented by a rule, it is unrepresentable: the join to the ledger is 1:1 once the query is pinned to one dimension.

ACROSS dimensions is the opposite, and it is the whole hazard of putting them in one table. A single turn commonly carries three or four contexts at once — measured: 3,816 records carry agent+mcp_tool+mcp_server, 2,201 carry agent+skill+plugin, 9 carry all five — and each of those rows names the turn's FULL cost, because each is a complete answer to a different question. A query that forgets to pin one dimension joins that turn once per context and reports up to five times its real cost. See store.SummarizeTurnContext, which takes the dimension as a required argument rather than as a filter that could be omitted.

A turn context is deliberately NOT an ActivityEvent kind, for the same reason skill context never was: tool-call attribution and turn-context attribution are different partitions of the same dollars, each honest alone and meaningless added together. Sharing activity_events would have put that mistake one forgotten WHERE clause away — SummarizeActivity grouped by tool, with no kind filter, would silently have counted every attributed turn again. Nor is it a column on activity_events: 41.8% of skill-context records carry no tool_use block at all and emit no activity row to hang a column on, and the agent dimension covers turns that called nothing far more often still.

NESTING is real and is recorded shallowly, because that is all the source offers. A skill may invoke another skill, but once the inner one is running the field names ONLY the inner skill, so cost lands on the innermost active one and an outer skill is not credited with what its callee spent. The same holds for an agent that spawns an agent. That is the source's own accounting, not a choice made here, and inventing a parent chain the transcript does not record would be a guess wearing a number.

PRIVACY: the NAME and nothing else — agent type, skill name, MCP server and tool name, plugin name. There is no field for arguments, inputs, prompts or results, and there is no raw column, so `privacy.no_raw` has nothing to drop here: the discipline is satisfied by the shape rather than by a switch.

type TurnDimension

type TurnDimension string

TurnDimension names one axis of turn attribution: the kind of thing a turn was running UNDER. The vocabulary is closed and enforced by a CHECK constraint on usage_turn_context — a new dimension is a schema change, not a string an adapter may invent.

THE SIX PARTITIONS. These five, plus the tool-call attribution in activity_events, are SIX PARTITIONS OF THE SAME DOLLARS. Every one of them divides one window's spend a different way — which agent ran, which skill was active, which MCP server answered, which plugin supplied the code, which tool was called — the way cost-by-region and cost-by-product are two views of one budget. Each is honest alone. Any query that sums across two of them counts the same tokens twice, and no amount of care at the call site is a substitute for making that unexpressible, which is why the read API takes exactly one dimension per query and refuses to group by "dimension" at all.

const (
	// DimensionAgent is the subagent type a turn ran as (claude-code's
	// `attributionAgent`: "workflow-subagent", "general-purpose", "Explore").
	// It is by far the largest of the five: measured over this machine's
	// transcripts, 79,816 of 102,887 usage-bearing assistant records carry it,
	// i.e. 77.6% of all token-bearing turns are subagent work that the tool-call
	// ledger describes with a couple of hundred `Agent` rows.
	DimensionAgent TurnDimension = "agent"
	// DimensionSkill is the skill a turn ran inside (`attributionSkill`). An
	// activity row of kind=skill records the turn that INVOKED a skill — one
	// call — while this records every turn the skill then spent: 44 invocation
	// rows against 8,039 records of actual work.
	DimensionSkill TurnDimension = "skill"
	// DimensionMCPTool is the MCP tool a turn was serving
	// (`attributionMcpTool`).
	DimensionMCPTool TurnDimension = "mcp_tool"
	// DimensionMCPServer is the MCP server that tool belongs to
	// (`attributionMcpServer`). Measured: it and DimensionMCPTool ALWAYS
	// co-occur — 7,084 records each, over the same 3,265 message ids, with no
	// record carrying one without the other. They are still two dimensions
	// rather than one composite value, because "what did the ruflo server cost"
	// and "what did browser_eval cost" are different questions and a composite
	// string would answer neither without parsing.
	DimensionMCPServer TurnDimension = "mcp_server"
	// DimensionPlugin is the plugin a turn's skill or agent came from
	// (`attributionPlugin`).
	DimensionPlugin TurnDimension = "plugin"
)

func TurnDimensions

func TurnDimensions() []TurnDimension

TurnDimensions returns the closed dimension vocabulary. The slice is a copy: a caller iterating it must not be able to edit the constant set.

func (TurnDimension) Valid

func (d TurnDimension) Valid() bool

Valid reports whether d is one of the known dimensions. An unknown dimension is refused at the API boundary rather than passed to SQL, so a typo returns an error instead of an empty result that looks like "this agent cost nothing".

type UsageEvent

type UsageEvent struct {
	// ID is the ledger row id (usage_events.id), set by event listing and left
	// zero on an event that has not been read back from the store. It is the
	// keyset-pagination handle: ids are AUTOINCREMENT, so they never repeat and
	// their order agrees with insertion order. It stays out of the serialised
	// event shape (json:"-") because it is storage identity rather than
	// observed usage, and the export key set is a pinned contract.
	ID    int64  `json:"-"`
	Tool  string // agent CLI id (ToolClaudeCode, ...) — categorisation dim
	Model string // model id — categorisation dim
	// Provider is the billing identity behind the request (ProviderAnthropic,
	// ...), taken from the source data when it names one. Empty means unknown.
	Provider string
	// ServiceTier is the provider's service tier for the request ("standard",
	// "batch", "priority", ...). Empty when the source reports none.
	ServiceTier string
	SessionID   string    // provider session id
	Project     string    // workspace / cwd path
	EventTime   time.Time // when the usage actually occurred (from the source)
	// ObservedTime is when the daemon read/stored the record. For aggregate
	// deltas (no real event time) EventTime is set equal to ObservedTime.
	ObservedTime time.Time

	InputTokens         int64
	OutputTokens        int64
	CacheCreationTokens int64
	CacheReadTokens     int64
	ReasoningTokens     int64 // optional subset of output (e.g. codex)
	// TotalTokens is provider-authoritative; each adapter sets it correctly for
	// its provider's accounting (cache tokens are separate for Anthropic but a
	// subset of input for OpenAI/codex — adapters must not double count).
	TotalTokens int64
	// CacheTTL splits CacheCreationTokens by requested cache lifetime. It is
	// TRANSIENT adapter enrichment consumed by the pricing stamp and is never
	// persisted: the ledger has no column for it and the insert statements list
	// their columns explicitly. json:"-" keeps it out of exports too.
	CacheTTL CacheWriteTTL `json:"-"`

	// CostMicroUSD is the cost stamped at collect time, in millionths of USD,
	// or nil when the event could not be priced. nil is the ONLY "unknown":
	// a stamped 0 would claim the request was free.
	CostMicroUSD *int64
	// PriceSource names the table that produced CostMicroUSD ("override",
	// "litellm-<fetch date>", "embedded-<snapshot date>"), or the "+"-joined
	// composite of the tables that produced it together
	// ("override+litellm-<fetch date>") when a partial override was completed
	// from the rung it displaced. A row billed off a model's long-context rate
	// card carries a "+long-context" suffix as well. Empty when unpriced, so a
	// later correction knows which table it corrects.
	//
	// The vocabulary is open: the value is stored, exported and displayed
	// verbatim and nothing parses it, so a new rung or a new composite may
	// appear without a schema change. Treat it as an opaque label.
	PriceSource string

	RequestID  string // provider request id (if any)
	MessageID  string // provider message id (if any)
	SourcePath string // file/db the record came from
	DedupKey   string // globally-unique stable key; inserts conflict-skip on this
	Kind       EventKind
	// Raw is the provider usage payload kept for audit (optional). Adapters
	// build it from an explicit allow-list of usage/model/identity fields, so
	// it never carries message content; config privacy.no_raw drops it
	// entirely. It is NOT a backfill source — the schema columns carry
	// everything cost and reporting need — and it is never marshalled by
	// default: export restores it only behind --include-raw, since rows
	// appended before the allow-list landed still hold whole transcript lines.
	Raw string `json:"-"`
}

UsageEvent is one immutable observed usage record. Stored append-only and deduplicated on DedupKey. All token counts are non-negative.

func (UsageEvent) ComputedTotal

func (e UsageEvent) ComputedTotal() int64

ComputedTotal sums the token components using Anthropic-style accounting (cache tokens additive). Adapters that lack a provider total may use it.

func (UsageEvent) Cost

func (e UsageEvent) Cost() (int64, bool)

Cost returns the stamped cost in micro-USD and whether the event carries one. A false second result means unpriced — not free.

func (*UsageEvent) SetCost

func (e *UsageEvent) SetCost(microUSD int64, source string)

SetCost stamps a priced cost and the price table that produced it. Callers that cannot price an event must leave both fields alone rather than stamp 0.

type VerificationTier

type VerificationTier string

VerificationTier is how well the adapter behind a tool is verified, in CONTEXT.md's vocabulary.

const (
	// TierLive: verified against sessions actually run on a real install.
	TierLive VerificationTier = "live"
	// TierFixture: the surface format comes from a trusted source and is
	// verified against constructed fixtures, not against a real log.
	TierFixture VerificationTier = "fixture"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL