Documentation
¶
Overview ¶
Package tokens is the accounting layer: the one message shape every source produces, the one fold over a stream of them, the day file, and the readers for the five declared source kinds.
The shape of the thing is one sentence. Each source hands the fold a stream of messages — {day, basis, model, repo, usage} — and the fold is ONE function over that stream, keyed exactly by (day, model, repo), with the five token types kept apart and a type no source reported left as a dash. What differs per source kind is only how the stream is produced, which is why the attribution rule, the dash rule and the sum rule are each written once here rather than once per reader: the prototype this replaces had two copies of the attribution table in two scripts and they disagreed about three repos.
Nothing in this package removes a file, and nothing in it runs a program except the one subprocess the OpenCode reader declares.
Index ¶
- Constants
- Variables
- func AllowedNote(name string, dir bool) bool
- func Applies(kind, field string) bool
- func BodyLine(day, who, model, repo string, t Type, count int64, basis string) string
- func DayOfStamp(stamp string) (string, bool)
- func HaveSQLite() error
- func HolderPID(path string) string
- func KnownParser(name string) bool
- func Label(kind, name string) string
- func MergeDay(old, fresh []DayRow, declared []string) (rows []DayRow, retained int, partials []Partial)
- func MissingDays(days []string) []string
- func ParseDayFile(name, text string) (DayFile, []Finding)
- func ParseMicro(s string) (int64, bool)
- func ParseSubject(subject string) (parsedSubject, bool)
- func ParserColumns(kind string) []string
- func Path(out, day string) string
- func PathStem(tok string) string
- func PathTokens(inputs []string) []string
- func Percent(part, whole int64) string
- func ReadDayFile(path string) (DayFile, []Finding, error)
- func ReadNoSpendFile(path string) (map[string]bool, error)
- func SourcesOf(rows []DayRow) []string
- func Subject(day, at, build string, supersedes []string) string
- func TakeFoldLock(out string, wait time.Duration) (func(), error)
- func Usd(micro int64) string
- func UsdPerMtok(micro, tokens int64) string
- func ValidDay(s string) bool
- func ValidMonth(s string) bool
- func ValidNoteID(id string) bool
- func ValidZone(s string) bool
- type Agg
- type BadDayFile
- type CheckOptions
- type CheckResult
- type ClaudeBound
- type Conflict
- type Counts
- type DayFile
- type DayRow
- type FileFinding
- type Finding
- type Folder
- type Key
- type Message
- type Mixed
- type ModelSum
- type Overlap
- type Pair
- type Partial
- type Row
- type Rules
- func (r *Rules) Attribute(tokens []string, prev string) string
- func (r *Rules) AttributeInputs(inputs []string, prev string) string
- func (r *Rules) FilterDay(day string)
- func (r *Rules) SetDay(day string)
- func (r *Rules) TotalUnattributed() int
- func (r *Rules) Unattributed() []UnattributedStem
- func (r *Rules) WatchUnattributed()
- type SessionSum
- type Shrink
- type Source
- func ReadBus(dir string, fsys fs.FS, rules *Rules, at time.Time) []*Source
- func ReadClaude(label, dir string, fsys fs.FS, rules *Rules, bounds ...ClaudeBound) *Source
- func ReadOpenCode(label, dbPath, scratch string, timeout time.Duration, rules *Rules) *Source
- func ReadProvider(kind, name, path string, _ *Rules) *Source
- func ReadSwarm(label, pool string, fsys fs.FS, rules *Rules) *Source
- type Stat
- type Sum
- type Superseded
- type Touched
- type Type
- type UnattributedStem
- type Unparsed
- type Unreadable
- type Weights
- type XaiUsageMissingError
- type XaiUsageNotFileError
Constants ¶
const ( PartialBlended = "blended" PartialCollision = "collision" )
PartialBlended and PartialCollision are the two reasons a row of the file on disk can be neither kept nor recomputed by this fold.
const ( Unknown = "unknown" Other = "other" Unattributed = "unattributed" )
The bucket names, which are repo names like any other and are never invented per source.
const ( KindClaude = "claude" KindOpenCode = "opencode" KindSwarm = "swarm" KindBus = "bus" KindProvider = "provider" )
The five source kinds, as `kind=` prints them.
const BusDateLayout = "Mon Jan 2 15:04:05 UTC 2006"
BusDateLayout is how nova-bus writes a note's Date line.
const Dash = "-"
Dash is the cell of a type the source did not report. It is not a zero: a zero is a measurement and a dash is an absence, and a zero that meant "not measured" would sum into a month claiming to be complete.
const DefaultMaxClaudeFiles = 20000
DefaultMaxClaudeFiles is the ceiling one --claude tree starts under.
const DefaultTimeout = 120 * time.Second
DefaultTimeout is the per-subprocess budget. It is the one flag with a default, for the reason SPEC.md gives nova-bus --git-timeout: it is how long the tool waits before saying so, not a fact about anybody's data.
const FileSuffix = ".tsv"
FileSuffix is a day file's extension.
const LockName = "fold.lock"
LockName is the lock file, inside the output directory: the thing being protected is that directory's day files, so the lock belongs beside them.
const LockWait = 10 * time.Second
LockWait is how long a second fold waits before refusing. It is bounded on purpose: a wait with no end is a tool that has stopped saying anything.
const SQLiteBinary = "sqlite3"
SQLiteBinary is the one program this family requires on PATH.
const SessionLabel = "claude-session"
SessionLabel is the source label the row names, so every number stays traceable to the flag of the run that wrote it.
const SubjectPrefix = "tokens "
SubjectPrefix opens every tokens note's subject, exactly: lower case, one blank.
const TempSuffix = ".tsv.tmp"
TempSuffix is the fixed temp name of the legacy day-file temporary, a file shape check still steps over: a temporary of this shape is a live input, and `check` reads it as a step-over rather than reporting it as a stray. The writer goes through pkg/atomicfile and produces unique `.<day>.tsv.tmp-<rand>` siblings, so nothing this tool writes carries this plain suffix.
const UTC = "utc"
UTC is the day basis of every row dated from a stamp. Anything else is the zone a provider's export declares for its own per-day totals, and a row that is not UTC says so in its own column rather than being quietly counted as one.
const Version = "nova-tokens v1"
Version is the first token of every day file's first line. A file whose first line is not this is refused by `sum` and named by `check`, and the repair is `fold --day <d>`.
Variables ¶
var AllTypes = []Type{Input, Output, CacheWrite, CacheRead, Reasoning}
AllTypes is every type, for a source that reports all five.
var ClaudeTypes = []Type{Input, Output, CacheWrite, CacheRead}
ClaudeTypes is what a Claude Code transcript carries: no reasoning count exists in it.
var Columns = []string{"date", "model", "repo", "input", "output", "cache_write", "cache_read", "reasoning", "rough", "day_basis", "sources"}
Columns are the eleven, in order, and the file's second line is exactly these.
A day file written while a twelfth column, `units`, existed carries that column in its header and on every row. The reader takes that file and ignores the column (LegacyColumns); the writer never writes it, so the day after a fold the file is eleven wide. The version line is unchanged, `nova-tokens v1`: the eleven columns mean exactly what they always did.
var DefaultWeights = Weights{Input: 1, CacheWrite: 1.25, CacheRead: 0.1, Output: 5}
DefaultWeights is a comparison, not a price: the ratios of one vendor's published list prices (cache write 1.25x an input token, cache read 0.1x, output 5x). The session verb's --weights flag carries these four numbers and takes yours.
var HeaderLine = strings.Join(Columns, "\t")
HeaderLine is the second line of every day file this tool writes.
LegacyColumns is the twelve-column header of a day file written while the `units` column existed. Only the reader knows it.
var LegacyHeaderLine = strings.Join(LegacyColumns, "\t")
LegacyHeaderLine is that file's second line.
var Parsers = []string{"google", "openai", "xai"}
Parsers are the export shapes this tool knows, one per provider. A label that is not one of them is a bad invocation rather than an unreadable file: the caller named a parser that does not exist.
var SwarmColumns = []string{
"job", "attempt", "from", "started", "ended", "end", "rc", "provider",
"model", "repo", "tokens_in", "tokens_out", "cache_write", "cache_read", "reasoning", "usd",
}
SwarmColumns are the sixteen usage-file columns, in the order the swarm writes them: `job`, `attempt`, `from`, `started`, `ended`, `end`, `rc`, `provider`, `model`, `repo`, `tokens_in`, `tokens_out`, `cache_write`, `cache_read`, `reasoning`, `usd`. A header that is not these is refused by name. The reader looks every cell up BY NAME, so this list is the whole contract with the swarm.
var TypeNames = [NTypes]string{"input", "output", "cache_write", "cache_read", "reasoning"}
TypeNames are the five names as a source, a bus line and a day file spell them.
Functions ¶
func AllowedNote ¶ added in v1.2.3
AllowedNote is the allowlist: the non-day entries a person keeps beside their day files. It is deliberately a list of SHAPES and not of names — a tool with `README.md` compiled into it would be a tool that special-cases one bench's directory — and every shape here is one this repository's own reports/tokens holds: a README and a session note (`*.md`), a collator's log (`*.log`), and the archive of what the day files replaced (`pre-*/`).
func BodyLine ¶
BodyLine renders one body line, which is what fold --bus parses. Every stored field goes through oneline.Field for the reason the day file's cells do: six tab-separated fields is a promise, and a model id holding a tab would make it seven.
func DayOfStamp ¶
DayOfStamp is the UTC day of an RFC 3339 stamp. The stamp is PARSED and converted, never sliced: its first ten characters are the day in whatever zone it was printed in, so a stamp offset from UTC can belong to a different UTC day. A stamp this tool cannot read is not a day, is not dated by a guess, and is not dropped: every caller counts and prints it.
func HaveSQLite ¶
func HaveSQLite() error
HaveSQLite reports whether the one required program is on PATH.
func HolderPID ¶
HolderPID is the pid in the lock file's holder stamp, or a dash when it holds none. It reads filelock's stamp and the bare pid an earlier binary wrote alike. It is read rather than scanned for: this tool never looks at another process's command line.
func KnownParser ¶
KnownParser reports whether a --provider kind names a parser.
func Label ¶
Label is how a source names itself in a row's sources column and on its own line: the kind, a colon, and the flag's label — or, for a bus lane, the lane owner's name, whatever the `who` field of a line inside it says.
func MergeDay ¶ added in v1.2.3
func MergeDay(old, fresh []DayRow, declared []string) (rows []DayRow, retained int, partials []Partial)
MergeDay merges this run's recomputed rows into the rows already in the day file.
declared is the set of source labels this run read, the same labels that land in a row's sources column. For each row of old:
- every source outside declared: RETAINED, cell for cell, because no source this run read contributed to it and this run has nothing truer to say about it;
- every source inside declared: REPLACED by this run's row for that (model, repo), and never summed with it, so no source is counted twice;
- some inside and some outside: a Partial, PartialBlended. Its cells are already a sum over both and nothing on disk takes them apart.
A retained row and a recomputed row with the same (model, repo, unit) is a Partial too, PartialCollision: the day file's rows are unique by (model, repo, unit), and summing the two would blend two runs' arithmetic into one cell no later fold could undo.
The returned rows are sorted by (model, repo), which is what ParseDayFile demands. When any Partial is returned the caller writes NOTHING: the rows are what the merge would have been, and are not a file.
func MissingDays ¶
MissingDays is every day between the first and the last with no file. A missing day is NAMED and never filled: nobody folded it, and the tool will not invent what nobody measured.
func ParseDayFile ¶
ParseDayFile reads a day file and returns everything wrong with it rather than the first thing: a run over a month wants one line per finding, and a reader sent back once per problem is a reader sent back eight times.
name is the file's own name without its suffix, which the `date` column must equal.
func ParseMicro ¶ added in v1.2.3
ParseMicro reads a decimal dollar amount ("1.23", "0.000001", the int64 micro-dollars down to six fractional digits) into micro-dollars. Any other text -- a dash, an empty cell, an exponent that no usage column writes -- is the second return false, an absence rather than a zero.
func ParseSubject ¶
ParseSubject reads a tokens note's subject. The second return is false when the subject is not a tokens note's at all — a different case from a tokens note whose trailer is wrong, which is a note refused by name.
func ParserColumns ¶
ParserColumns is the sorted list of column names one parser reads, for a refusal that says what the file WANTS and not only what was wrong.
func PathStem ¶ added in v1.2.3
PathStem is the leading directory a token names, to stemDepth elements. It is what the unattributed tally counts, and it is written here rather than in the command so that the listing and the rule it suggests are cut from the same string.
func PathTokens ¶
PathTokens is every path-like token in the strings given: one starting with `/`, `~`, or a `<host>:<org>/<repo>` remote, and continuing over path characters.
func Percent ¶
Percent renders a bucket's share of a day to one decimal, so a day that is 40% unknown says so on the line a person reads.
func ReadDayFile ¶
ReadDayFile reads and parses one day file from disk.
func ReadNoSpendFile ¶ added in v1.2.3
ReadNoSpendFile reads --no-spend: one YYYY-MM-DD per line, with a blank line and a `#` comment skipped and anything after the day on a line taken as its note, so the list can carry WHY a day had no spend. A line that is not a day is an error naming it, because a list half-read is a gate half-shut.
func SourcesOf ¶ added in v1.2.3
SourcesOf is the union of every row's sources, sorted: what the version line's `sources=` says about the file that was actually written, retained rows included.
func Subject ¶
Subject renders a tokens note's subject: the date, and the tool's one trailer, which is where the stamp, the build id and a correction's predecessor set travel.
func TakeFoldLock ¶
TakeFoldLock takes the output directory's lock, waiting up to wait, and returns the release. The release is safe to call more than once.
func Usd ¶ added in v1.2.3
Usd renders micro-dollars as dollars, trimming trailing zeros after the point: 1234001 is "1.234001" and 2000000 is "2". A negative cost keeps its sign.
func UsdPerMtok ¶ added in v1.2.3
UsdPerMtok is the average cost, in dollars per million tokens, to four decimals. Zero tokens is the dash, never a division: there is no average over nothing.
func ValidDay ¶
ValidDay reports whether s is a YYYY-MM-DD day ON THE CALENDAR. The shape alone is not enough: an impossible day that passes the shape check would be written as a day file, pass `check`, and leave MissingDays walking from a day that does not exist. time.Parse is the range check, and the round trip refuses what it normalises (2026-02-30 -> 2026-03-02).
func ValidMonth ¶ added in v1.2.3
ValidMonth reports whether s is a YYYY-MM calendar month.
func ValidNoteID ¶
ValidNoteID reports whether an id has the shape nova-bus assigns. `report --supersedes` checks the shape and nothing else, because the lane is not on that machine.
Types ¶
type Agg ¶
type Agg struct {
Totals [NTypes]int64
Dashes [NTypes]int
Rows int
Rough int
NonUTC int
// contains filtered or unexported fields
}
Agg is one grouping's totals.
func (*Agg) Cell ¶
Cell is one type's total as a month prints it: the number, or `-` when NO row in this grouping reported that type at all. A type the source did not report is `-` in the cell, never 0 -- a zero there would be summed into a month claiming to be complete -- and the dashes= tuple beside it is a correction a reader has to know the column order of.
type BadDayFile ¶
type BadDayFile struct{ Path, Reason string }
BadDayFile is a file under --out that `sum` will not read.
func (*BadDayFile) Error ¶
func (e *BadDayFile) Error() string
type CheckOptions ¶ added in v1.2.3
type CheckOptions struct {
// Strict names every calendar gap as missing and every non-day entry as a stray.
Strict bool
// NoSpend is the days a person has recorded as having had no spend, from
// --no-spend. When it is non-nil, every gap day NOT in it is missing: the caller has
// claimed to know the whole range, so a gap the list does not name is a day nobody
// folded.
NoSpend map[string]bool
// Through is the day the ledger is expected to be current through, from --through.
// When set, a last folded day before it marks the result Stale.
Through string
}
CheckOptions is how a caller reads the two judgement calls above. The zero value is the default reading: gaps and notes are counted, not named.
type CheckResult ¶
type CheckResult struct {
Files int
Rows int
First string
Last string
Findings []FileFinding
// Missing is the days named as findings; Gaps is every calendar day in the range
// with no file, whether or not it was named. Gaps is the larger fact, and is what
// gap=<n> prints.
Missing []string
Gaps []string
// Strays is what is named; Notes is the non-day entries the allowlist stepped over.
Strays []string
Notes []string
Stale bool
}
CheckResult is a whole output directory, checked.
func Check ¶
func Check(out string, opt CheckOptions) (*CheckResult, error)
Check walks every file under out. A day file is parsed; recognized day-file temporaries and the lock are stepped over, because the wreckage of a killed fold is not a stray; a non-day entry the allowlist admits is counted as a note; anything else is named and LEFT ALONE — this tool removes nothing, and a person removes a stray.
type ClaudeBound ¶ added in v1.2.3
ClaudeBound is the ceiling and the excludes one --claude tree is walked under: at most MaxFiles transcript files, with every path matching an Exclude glob (or lying under one) left out. The zero value reads the whole tree, the behavior before the bound existed. DefaultMaxClaudeFiles is the ceiling the tool states in its help.
The largest plausible state is 3,000 transcript files a day (docs/SPEC-TOKENS.md rule 11), so the 20,000-file default sits far above a real tree and still stops the very large cost a temporary tree included by mistake produced for a cold reader.
type Conflict ¶
Conflict is a lane-day with more than one tip: no winner is inferred, nothing folds.
type Counts ¶
type Counts struct {
// contains filtered or unexported fields
}
Counts is the five types, each either a count or absent.
func (*Counts) Add ¶
Add folds one Counts into another, per type, over the types the other reported.
func (Counts) Billed ¶ added in v1.2.3
Total is the five types summed, for the shares on a day line. A dash adds nothing. Billed is the tokens a cost covers: input, output, cache write and cache read, the four types a provider prices. Reasoning is its own column and in none of them.
type DayFile ¶
type DayFile struct {
Day string
At string
Build string
Turns string // a decimal integer, or a dash
Sources []string
Rows []DayRow
}
DayFile is a whole day file, parsed or about to be written.
func (*DayFile) Render ¶
Render is the file's bytes.
EVERY STORED CELL GOES THROUGH oneline.Field, and that is what makes eleven tab-separated columns a promise rather than a hope: a model id or a repo name holding a tab would otherwise write a twelve-cell row, and one holding a newline would write two rows, from a value this tool copied out of somebody's transcript. Field escapes whitespace and "=", so an ordinary name is untouched and a hostile one is one token.
func (*DayFile) Save ¶
Save writes the file whole atomically via pkg/atomicfile (exclusive temporary file beside target, explicit mode, fsync to media, atomic rename). Nothing is appended and nothing is edited in place. It refuses to write a file whose rendered count cell is neither a non-negative integer nor "-" (security#75 finding 1).
type DayRow ¶
type DayRow struct {
Date, Model, Repo string
Counts Counts
Rough int
Basis string
Sources []string
}
DayRow is one written row.
type FileFinding ¶
FileFinding is one thing wrong with one file.
type Finding ¶
Finding is one thing wrong with a file on disk, with the line it is on (0 for the file as a whole).
type Folder ¶
type Folder struct {
// contains filtered or unexported fields
}
Folder is the fold: the one function over the stream, and the only place a row is made.
func (*Folder) Add ¶
Add folds one message from the source named by label. A repeated id from another source of the same provider is recorded and kept: the write is refused later, the message is not dropped (TokenFold, OverlapRefusedBeforeWrite and UnscopedIDNotDeduped).
func (*Folder) DayRows ¶
DayRows is the day's rows, sorted by (model, repo), and separately the rows of that day that mixed two bases. A mixed row is in neither the file nor the counts.
func (*Folder) Overlaps ¶
Overlaps is every pair of sources that shared an id, sorted, so the remedy can name one.
func (*Folder) RefuseWrite ¶ added in v1.2.3
RefuseWrite reports a detected overlap. TokenFold invariant OverlapRefusedBeforeWrite: the fold is refused before any day file is written.
type Key ¶
type Key struct{ Day, Model, Repo string }
Key is exactly (day, model, repo). Nobody's name is in it: the `who` of a bus line and the window-or-child mark of a transcript are not columns, because a model on a repo on a day is one row whoever drove it.
type Message ¶
type Message struct {
ID string // the source's own message id, for the overlap check; not part of Key
Day string // YYYY-MM-DD
Basis string // UTC, or the zone a provider export declares
Model string
Repo string // already attributed by the reader, through repo.go's one function
Counts Counts
Rough int // how many `~` bus lines this message stands for
Turn bool // counted into turns= (the sources that count messages)
Usd int64 // micro-dollars, from a usage `usd` column or a cost tick; 0 where absent
Priced bool // a source reported Usd for this message; false is "no cost given", never a zero cost
Provider string // the provider prefix for model= on an AVG line; "" where unknown
}
Message is the one unit every source hands the fold.
type Mixed ¶
Mixed is a row fed by two day bases: not written, and named. Labels is WHICH sources fed it: "if a row mixed two day bases it names the two labels" (SPEC-TOKENS, the TOKENS NOTE paragraph). Telling a caller to declare one export for that day without saying which two are competing is the one remedy that names nothing.
type Overlap ¶
Overlap is two declared sources of one provider that fed the same message ids. TokenFold invariant OverlapRefusedBeforeWrite: it is a refusal before any day file is written, not a drop of either message.
type Partial ¶ added in v1.2.3
type Partial struct {
Day, Model, Repo string
Sources []string // the row's own sources cell
Folded []string // the labels this run declared, sorted
Why string // PartialBlended or PartialCollision
}
Partial is one row this fold can neither retain nor replace. It is a refusal, not a warning: nothing of the day is written while one stands.
type Row ¶
type Row struct {
Key
Counts Counts
Rough int
Usd int64 // micro-dollars summed over the messages that fed the row
Priced bool // some message that fed the row reported a cost; false: Usd is no measurement
// PricedTokens is the Billed tokens of the messages that reported a cost: what Usd
// covers, and the only tokens a rate over Usd may divide by.
PricedTokens int64
Provider string // the provider prefix of the messages that fed the row; "" where unknown
// contains filtered or unexported fields
}
Row is one line of a day file while it is still being accumulated.
func (*Row) Bases ¶
Bases is the day bases that fed this row, sorted. More than one is a row that is not written: a row must carry one day basis, and a mixed row is named and left out of the file and the counts.
type Rules ¶
type Rules struct {
// contains filtered or unexported fields
}
Rules is a rules file: lines of <name><TAB><regexp>, in priority order.
It optionally keeps the tally behind `sources --unattributed`: the path stems that were SEEN and matched no rule, which is the evidence a person needs to improve the file. `other=81%` on a day line says the rules are not good enough; it does not say which line to add, and until this tally existed nobody could find out without grepping transcripts by hand.
func LoadRules ¶
LoadRules reads the caller's rules file. A malformed line is an error naming the line, because a rules file half-read is a repo column nobody can defend.
func (*Rules) Attribute ¶
Attribute is THE statement of the rule, and every reader reaches a repo name through it:
take every path-like token in the message's tool-call inputs for each token, in order: the first rule whose regexp matches names the repo; stop a token that matches no rule counts as "seen" no token matched a rule, at least one token seen -> other no token at all -> the stream's previous repo no previous repo -> unknown
prev is the stream's previous repo — per transcript file, per OpenCode session, and never across sources.
The `other` arm is also where the unattributed tally is taken, when a caller asked for it: the tokens that reached it are exactly the ones a better rules file would name.
func (*Rules) AttributeInputs ¶
AttributeInputs is Attribute over the raw strings of a message's tool-call inputs, with the path-like tokens pulled out of them first. A reader that carried its own regexp would be the second copy of the rule, so the extraction is here too.
func (*Rules) FilterDay ¶ added in v1.2.3
FilterDay limits unattributed tallying to messages on the named day. When day is empty, all days are tallied.
func (*Rules) SetDay ¶ added in v1.2.3
SetDay sets the day of the message currently being attributed.
func (*Rules) TotalUnattributed ¶ added in v1.2.3
TotalUnattributed is every path token that fell to `other`, counted whether or not its stem got a place in the tally.
func (*Rules) Unattributed ¶ added in v1.2.3
func (r *Rules) Unattributed() []UnattributedStem
Unattributed is the tally, heaviest first and then by stem, so two runs over one tree print the same list in the same order.
func (*Rules) WatchUnattributed ¶ added in v1.2.3
func (r *Rules) WatchUnattributed()
WatchUnattributed turns the tally on. It is called before any source is read, and never by fold: `sources --unattributed` is a verb that only looks.
type SessionSum ¶ added in v1.2.3
type SessionSum struct {
Turns int
Input int64
CacheWrite int64
CacheRead int64
Output int64
// Days is the per-day split, keyed by the UTC day of each turn's stamp. A session that
// crosses midnight is two rows, never one row dated by the file.
Days map[string]*SessionSum
// Unstamped counts the turns whose stamp this tool could not read. They are in the
// totals and in no day: a turn measured but not dated is named, never dropped and
// never dated by a guess.
Unstamped int
// Models is every model the transcript names, sorted, and Unnamed counts the turns that
// name none. A turn with no model cannot be booked under one, and is never booked under
// a guess: the fold refuses while Unnamed is above zero.
Models []string
Unnamed int
// DayModels is the split of Days by the model of each turn: day, then model. A session
// that changes model mid-window is one row per model, never one row under the last.
DayModels map[string]map[string]*SessionSum
}
SessionSum is one session's usage: the turns, the four counts, and the days the turns fell on so a fold can write the right day file.
func ReadClaudeSession ¶ added in v1.2.3
func ReadClaudeSession(path string) (SessionSum, error)
ReadClaudeSession sums one Claude Code session jsonl.
A STREAMED assistant message writes its id on many lines, each with a usage block that grows; the LAST line for an id is the message and every earlier one is a partial. So the sum is per message id, last write wins -- the same rule ReadClaude keeps -- and a reader that added every line would report a session several times its own size.
func (SessionSum) AvgContext ¶ added in v1.2.3
func (s SessionSum) AvgContext() int64
AvgContext is the average context a turn carried: everything read (input, cache write, cache read) over the turns. It is the number that says whether the window is the cost, and a session with no turn has none.
func (SessionSum) DayList ¶ added in v1.2.3
func (s SessionSum) DayList() []string
DayList is the days this session touched, sorted.
func (SessionSum) Line ¶ added in v1.2.3
func (s SessionSum) Line(w Weights) string
Line is the one line the session verb prints, WEIGHTED under w.
func (SessionSum) Rows ¶ added in v1.2.3
func (s SessionSum) Rows(day, role string) []DayRow
Rows is the day file rows one day of this session folds into: one per model the turns of that day name. The row names the model the TRANSCRIPT names and, when the caller's --role flag names a role, the seat too: `<model>/<role>`, with the role in the repo cell. The same model driving another line of work is a different row, because the question the ledger answers is what that work cost, not what the model cost; a window's turns are not one repo's work, and a row attributed to whichever repo a tool call happened to name would move the cost around from day to day, so with no role the model stands alone and the repo cell is the fixed word `unattributed`. A day the session has no turn on is a row of zeros for every model the session names, because "this window spent nothing that day" is a measurement; a session that names no model has no rows.
func (SessionSum) UnbookableReason ¶ added in v1.2.3
func (s SessionSum) UnbookableReason() string
UnbookableReason is why this session cannot be folded into the ledger, or "" when it can: every row is booked under the model the transcript names, so a transcript that names none (or names it on only some turns) has no honest row. The caller refuses with this text.
func (SessionSum) Weighted ¶ added in v1.2.3
func (s SessionSum) Weighted(w Weights) int64
Weighted is the fresh-input-equivalent under w: w.Input x input + w.CacheWrite x cache write + w.CacheRead x cache read + w.Output x output, rounded down to a whole token.
type Shrink ¶
type Shrink struct {
Day string
Type Type
File string // the number in the file
Now string // the number now, or a dash where the source went quiet
}
Shrink is one type that would go backwards.
func Shrinks ¶
Shrinks compares what is on disk with what a fold has just computed. A type that is lower, or that was a number and is a dash now, is a source that went quiet, and a source that went quiet must never silently lower a day's spend.
A dash in the file that is a number now is NOT a shrink: that is coverage arriving.
type Source ¶
type Source struct {
Label string // as it is printed and as it appears in a row's sources column
Kind string
Path string
Reports []Type
Basis string // utc, a zone, or "mixed" for a bus lane whose lines carry more than one
Stat Stat
Stream []Message
Unreadables []Unreadable
Unparseds []Unparsed
Supersededs []Superseded
Conflicts []Conflict
Toucheds []Touched
// contains filtered or unexported fields
}
Source is one declared source after it has been read.
func ReadBus ¶
ReadBus reads every lane the roster names and returns ONE source per lane, because the label of a self-report is the lane owner's name whatever the `who` field of a line inside it says. The lanes are read through the caller's fs.FS, rooted at dir (os.DirFS(dir) in main, fstest.MapFS in a test), and dir is the path the report names.
func ReadClaude ¶
ReadClaude walks a transcript tree and returns its stream and its accounting.
Every message carries no unit, which the fold writes as `-`: a transcript names a repo, never a piece of work. The tree is read through the caller's fs.FS, rooted at dir (os.DirFS(dir) in main, fstest.MapFS in a test), and dir is the path the report names.
The walk runs to completion BEFORE the first file is opened, so a tree over MaxFiles is refused with the totals it found and not one byte of it is read (SPEC-TOKENS rule 11: the state is bounded, and a source is a claim that the report covers it). An Exclude glob is the remedy for the temporary tree that made the tree overrun.
func ReadOpenCode ¶
ReadOpenCode copies the database into scratch and reads it there.
func ReadProvider ¶
ReadProvider reads one billing export with the parser its kind names. The label on every row it feeds is `<kind>:<name>`, a per-account label, as the day-file example writes it, so two exports from one provider are two sources.
func ReadSwarm ¶
ReadSwarm reads every <pool>/usage/*.tsv and nothing else. The pool is read through the caller's fs.FS, rooted at pool (os.DirFS(pool) in main, fstest.MapFS in a test), and pool is the path the report names.
func (*Source) AddMessage ¶
AddMessage puts one message into this source's stream, collapsed onto its id.
A streamed transcript repeats a message id on every line; the last line for an id carries the message's final usage, and that is the one counted. Within one source, a second occurrence of an id is dup=<n>, never a second count. A message with no id is counted in noid=<n> and not folded. The rule is written ONCE, here, and every reader collapses through it: two copies of one rule are two rules.
func (*Source) Collapse ¶
func (s *Source) Collapse()
Collapse lays the collapsed messages into Stream, in first-seen order, and counts them. Every reader that calls AddMessage ends with it.
func (*Source) ReportsList ¶
ReportsList is the comma-joined type names this source reports at all, so a reader of a mixed row can see which source could not have covered which cell.
type Stat ¶
type Stat struct {
Files int
Unreadable int
Messages int
Dup int
NoID int
NoUsage int
Unparsed int
Comments int
Redated int
Superseded int
Rows int
}
Stat is what one declared source yielded. A field that does not apply to the kind prints a dash rather than a zero: a transcript has no unparsed lines and a bus lane has no duplicate ids, and a dash is an absence where a zero is a measurement.
type Sum ¶
type Sum struct {
Month string
Days []string
Missing []string
Rows int
Turns int
HaveTurn bool
Pairs []Pair
Models []ModelSum
Total *Agg
}
Sum is a whole month.
type Superseded ¶
type Superseded struct{ Label, Note, By, Day string }
Superseded is a bus note a later note in the same lane replaced by name.
type Type ¶
type Type int
Type is one of the five token types. They are kept apart everywhere: nothing in this package folds one into another, and a provider that does not expose reasoning or cache usage is not evidence that none occurred.
The five, in the order every row prints them.
func TypeByName ¶
TypeByName resolves one of the five names.
type UnattributedStem ¶ added in v1.2.3
UnattributedStem is one path stem no rule named, with how many path tokens fell onto it.
type Unparsed ¶
type Unparsed struct {
Label, Note string
Line int
Text string
// Remedy, when set, is the one act for THIS unparsed rather than for its kind: a
// near-miss subject is a bus unparsed, and "a body line is date<TAB>who..." is not
// what its writer has to change.
Remedy string
}
Unparsed is a bus line, a whole bus note, or an export the parser could not read.
type Unreadable ¶
Unreadable is a declared source the tool could not read: counted, printed on its own line, and the reason the run exits 1, because a declared source is a claim that the report covers it. Line is the 1-based first line that could not be read (a badline); 0 when the failure is not a line, so a permission problem is not given a line number.
type Weights ¶ added in v1.2.3
type Weights struct{ Input, CacheWrite, CacheRead, Output float64 }
Weights is the four ratios the WEIGHTED equivalent is built from: how many fresh input tokens one input, one cache write, one cache read and one output token are worth.
func ParseWeights ¶ added in v1.2.3
ParseWeights reads the in,cw,cr,out four numbers --weights takes. A value that is not four finite numbers is a refusal saying what the flag WANTS, never a guess (docs/STANDARD.md section 3, onboarding point 2).
type XaiUsageMissingError ¶ added in v1.2.3
type XaiUsageMissingError struct {
Path string
}
XaiUsageMissingError is the refusal when --provider xai names a path that is not there. The path is the one the flag gave. No other path is opened.
func (*XaiUsageMissingError) Error ¶ added in v1.2.3
func (e *XaiUsageMissingError) Error() string
func (*XaiUsageMissingError) Unwrap ¶ added in v1.2.3
func (e *XaiUsageMissingError) Unwrap() error
type XaiUsageNotFileError ¶ added in v1.2.3
type XaiUsageNotFileError struct {
Path string
}
XaiUsageNotFileError is the refusal when --provider xai names something other than one regular file. A directory is not walked for usage.json files.
func (*XaiUsageNotFileError) Error ¶ added in v1.2.3
func (e *XaiUsageNotFileError) Error() string