cost

package
v0.22.0 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package cost prices what the agent spends. Every provider reports the tokens a call used; this package turns those counts into dollars, keeps a running ledger per session and overall, and refuses a turn once a budget cap is reached.

Prices come from the model catalog OpenRouter publishes — the one public list that carries per-token rates for every major vendor — cached on disk so a cold start is not a network round trip. Models served from this machine are free by construction, and anything the catalog does not carry is counted in tokens and left unpriced rather than guessed at.

Index

Constants

View Source
const DefaultPricesURL = "https://openrouter.ai/api/v1/models"

DefaultPricesURL is the public model catalog: ids with per-token rates for every vendor OpenRouter fronts, which is most of them.

Variables

This section is empty.

Functions

func BudgetStop

func BudgetStop(err error) string

BudgetStop returns the sentence to answer with when err is a budget refusal, and "" for every other error. A cap is a decision the user made, not a failure to report as one.

func FormatTokens

func FormatTokens(n int) string

FormatTokens abbreviates a token count for a line that has no room to spell it out.

func FormatUSD

func FormatUSD(v float64) string

FormatUSD writes an amount at the precision it deserves: cents once there are cents to see, four places while a turn still costs less than one.

func Local

func Local(c config.Candidate) bool

Local reports whether a candidate is served locally, and therefore costs nothing per token. A type Factor knows to be local says so on its own; any other type is judged by where its endpoint points.

func NewTool

func NewTool(m *Meter) []tools.Tool

NewTool builds the usage tool, or nothing at all when the meter is inactive: a tool that can only ever answer "not counting" is prompt weight.

Types

type BudgetError

type BudgetError struct {
	Scope string // session | day | month | total
	Spent float64
	Limit float64
}

BudgetError is a call refused because a cap was met.

func (*BudgetError) Error

func (e *BudgetError) Error() string

func (*BudgetError) Message

func (e *BudgetError) Message() string

Message is the sentence the user reads instead of an answer: what was spent, against which cap, and the two ways out of it.

type Catalog

type Catalog struct {
	// contains filtered or unexported fields
}

Catalog answers what a model costs. Overrides from config win, models served locally are free, and everything else comes from the fetched catalog — by exact id first, then by the vendor-and-date-insensitive form that lets a native Anthropic model id find its OpenRouter twin.

func NewCatalog

func NewCatalog(cfg config.CostConfig, candidates []config.Candidate, cachePath string) *Catalog

NewCatalog builds the price book for one configuration. It reads whatever the last fetch cached, so pricing works before — and without — a network call.

func (*Catalog) Fresh

func (c *Catalog) Fresh() bool

Fresh reports whether the cached catalog is young enough to skip a fetch.

func (*Catalog) Paid

func (c *Catalog) Paid() bool

Paid reports whether any configured candidate bills per token — the answer to "is this machine spending money when it thinks?".

func (*Catalog) Price

func (c *Catalog) Price(model string) (Price, bool)

Price returns what a model charges. ok is false when nothing prices it, which the caller must report as unpriced rather than as free.

func (*Catalog) Refresh

func (c *Catalog) Refresh(ctx context.Context) error

Refresh fetches the catalog and caches it. It is a no-op when nothing here bills per token: a machine running only local models has no rates to look up and no reason to phone out.

func (*Catalog) Watch

func (c *Catalog) Watch(ctx context.Context)

Watch keeps the price book current while ctx runs. A fetch that fails is a warning and nothing more: yesterday's prices are far better than none.

type ChatProvider

type ChatProvider interface {
	Chat(ctx context.Context, req *provider.Request) (*provider.Response, error)
}

ChatProvider is the seam the meter wraps: the failover chain, or anything else that answers a request.

type Ledger

type Ledger struct {
	// contains filtered or unexported fields
}

Ledger accumulates spend and keeps it on disk, so "what has this cost me" survives a restart. Each call is merged into the file rather than overwriting it: the gateway and a terminal session can both be spending at once, and neither should erase the other's total.

func NewLedger

func NewLedger(path string) *Ledger

NewLedger opens the ledger at path, reading whatever is already there. A file that cannot be read starts empty: losing a total is not worth failing startup over.

func (*Ledger) Flush

func (l *Ledger) Flush() error

Flush merges anything still pending into the file.

func (*Ledger) Record

func (l *Ledger) Record(sessionKey, model string, t Totals)

Record bills one provider call to a session and flushes it to disk. A write that fails keeps the call pending, so the next one carries both — worth saying once, because the totals are then only as durable as the process, but not worth saying on every call while the disk stays broken.

func (*Ledger) Snapshot

func (l *Ledger) Snapshot(sessionKey string) Snapshot

Snapshot reads the ledger for one session.

type Meter

type Meter struct {
	// contains filtered or unexported fields
}

Meter sits between the agent loop and the provider chain. Every call that comes back is priced and billed to the session that made it — including the ones the user never asked for, like compaction — and every call that goes out is checked against the budget first, because the only useful moment to stop is before the money is spent.

func NewMeter

func NewMeter(inner ChatProvider, catalog *Catalog, ledger *Ledger, cfg config.CostConfig) *Meter

NewMeter wires a meter around inner. It stays a pass-through unless tracking is on or a cap is set — a cap with tracking switched off would otherwise be a cap that never counts anything.

func (*Meter) Active

func (m *Meter) Active() bool

Active reports whether anything is being counted.

func (*Meter) Budget

func (m *Meter) Budget() config.BudgetConfig

Budget returns the caps in force.

func (*Meter) Chat

func (m *Meter) Chat(ctx context.Context, req *provider.Request) (*provider.Response, error)

func (*Meter) OverviewLine

func (m *Meter) OverviewLine() string

OverviewLine is the tray row: today against all time, or against the global cap when one is set — the same numbers, said the way the cap makes relevant.

func (*Meter) Report

func (m *Meter) Report(sessionKey string) string

Report renders the full usage picture for the agent and the CLI.

func (*Meter) SessionLine

func (m *Meter) SessionLine(sessionKey string) string

SessionLine is the status-bar segment: what this conversation has cost, and the cap it is spending against when there is one.

func (*Meter) Snapshot

func (m *Meter) Snapshot(sessionKey string) Snapshot

Snapshot reads the ledger as one session sees it.

func (*Meter) Unpriced

func (m *Meter) Unpriced(s Snapshot) []string

Unpriced names the models that have been used but that nothing prices — the reason a total can read lower than the invoice.

type Price

type Price = config.Price

Price is what one model charges, in USD per million tokens.

type Snapshot

type Snapshot struct {
	Session  Totals
	Day      Totals
	Month    Totals
	Total    Totals
	Models   map[string]Totals
	Sessions map[string]Totals
}

Snapshot is everything a report or a status line needs, read at once so the numbers in it agree with each other.

func (Snapshot) Spent

func (s Snapshot) Spent(period string) Totals

Spent returns what one budget scope has cost so far.

type Tool

type Tool struct{ Meter *Meter }

Tool lets the agent answer "what has this cost?" — for the conversation it is in, for today, for all time — and say how much room is left under a cap before it runs into one.

func (*Tool) Description

func (t *Tool) Description() string

func (*Tool) Execute

func (t *Tool) Execute(ctx context.Context, args map[string]any) *tools.Result

func (*Tool) Name

func (t *Tool) Name() string

func (*Tool) Parameters

func (t *Tool) Parameters() map[string]any

type Totals

type Totals struct {
	Input  int     `json:"input_tokens"`
	Output int     `json:"output_tokens"`
	USD    float64 `json:"usd"`
	Calls  int     `json:"calls"`
}

Totals is one bucket of spend: what went in, what came out, and what it cost. USD stays at zero for tokens nothing priced, so a report can say "unpriced" instead of implying "free".

func (Totals) Tokens

func (t Totals) Tokens() int

Tokens is the whole traffic of a bucket, which is what a status line has room for.

Jump to

Keyboard shortcuts

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