usage

package
v0.1.1271 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 37 Imported by: 0

Documentation

Overview

Package usage keeps the token count of every call the gateway serves, so magpie can show what each agent and model consumed and roughly what it cost. Records go to one JSON-lines file next to providers.json. Metadata leaves the machine only when OTLP export is explicitly enabled.

Index

Constants

View Source
const (
	SourceGateway = "through"
	SourceSession = "direct"
	// SourceReported is the third: the calls a client reported through
	// POST /v1/magpie/usage/requests (#1874), which went straight to their
	// vendor. Record.Source and Row.Source spell it the same, as does via=.
	SourceReported = "reported"
)

SourceGateway and SourceSession are the two sources a request row is of, which Row.Source spells "" and "log": the calls magpie's own gateway served, and the ones read from the agents' own session files, which it never saw. They are the requests page's source cells, and the values its via= takes.

View Source
const (
	CodexWindowMs   = 1000
	CodexBodyTokens = 100
)

A Codex turn of one call read from its session file is timed by the turn's time_to_first_token_ms and its end, and that window can be one network push, not the writing: Fallmonkey on Discord had a reply whose first token came at 6.8 s, the model working, and the rest 0.2 s later in one go, read as thousands of tok/s. Such a row's window after the first token tells a speed only when it is CodexWindowMs or more and its tokens (its answer, for one that reasoned) CodexBodyTokens or more; else it tells the least it wrote at over the whole turn (Row.Floor). app.js's ledDecode and ledFloor_ have the same.

View Source
const (
	MinDecodeMs    = 100
	MaxDecodeSpeed = 10000
)

A reply's speed is its output tokens over its decode window, the ms from its first content to its end (#196; one that reasoned, DecodeOf); a window counts only when it timed the writing (#731). One that is shorter than MinDecodeMs, or over which the tokens would have come faster than MaxDecodeSpeed a second, didn't: the reply came in one burst at its end, written before the window opened — a turn that is one big tool call from Gemini, whose functionCall comes whole, has its first content a millisecond before its end, and 8264 tokens in 1 ms read 8,264,000 tok/s. Such a reply tells no speed and is left out of the summed one, where its tokens over next to no time would lift everyone's; its TTFT still counts. The bounds are wide of any real stream: the fastest vendors write a few thousand tokens a second, and a tenth of a second is the least a stream's timing tells anything by. routing/client.js's speedOf has the same.

View Source
const (
	HeldShare = 4
	ThinkPace = 20
)

HeldShare and ThinkPace tell a reply held back and let go in a burst (DecodeOf): its content came in under a quarter of its window, or its answer at over 20 times the pace its reasoning came. routing/client.js's decodeOf and app.js's ledDecode have the same.

View Source
const BodyCut = "\n… (cut here by magpie)"

BodyCut ends a body the gateway captured only the start of.

View Source
const HeatmapWeeks = 53

HeatmapWeeks is how many weeks the heatmap shows, this one the last.

View Source
const LoopErrType = "reply_loop"

LoopErrType is a record's ErrType for a reply magpie ended because the model was stuck in a loop (#1359), told apart from what vendors call their errors.

View Source
const OtherComputers = "others"

OtherComputers is the Filter.Computer of every other computer's calls.

View Source
const SharedDays = 90

SharedDays is how many days of calls a computer keeps on the server.

View Source
const ThisComputer = "this"

ThisComputer is the Filter.Computer of the calls made here.

View Source
const UnknownProvider = "session-unknown"

UnknownProvider groups local session calls without claiming a supplier. Recorded provider IDs and matched creator identities stay in their own fields.

Variables

View Source
var Agents func() []Known

Agents lists the agents magpie knows. Package agent sets it from each agent's own description, so they are listed in one place only.

View Source
var CSVHeader = []string{"time", "agent", "requested_model", "provider", "host", "model", "served_model", "swapped",
	"effort", "input_tokens", "output_tokens", "cache_write_tokens", "cache_read_tokens", "reasoning_tokens",
	"cost_usd", "duration_ms", "ttft_ms", "status", "error", "session", "kind", "provider_key_id", "provider_key_name", "provider_account", "route_id",
	"request_id", "endpoint", "error_message", "error_type", "source", "rejected", "session_provider", "session_account", "session_official_login", "caller_key_id", "caller_key_name", "response_id", "upstream",
	"cache_write_5m_tokens", "cache_write_1h_tokens", "tries", "handed_on", "tier_asked", "tier_sent", "tier_served", "upstream_stop"}

CSVHeader is the ledger's columns, as WriteCSV writes them.

View Source
var Clock = time.Now

Clock is the time the periods are read by: when today began, the hours or days of the chart, the day a cached answer is for. A variable for the tests, which hold it still so that midnight never falls between a call they log and the period they ask for.

View Source
var Dimensions = []string{"provider", "agent", "model", "modelAt"}

Dimensions are what calls are told apart by: the provider they went to, the agent that made them, the model, and the model at the provider it went to ("modelAt"), so one model's speed at each provider can be compared (inaction on Discord).

View Source
var FloorAgents = map[string]bool{"claude": true, "claude-desktop": true, "codex": true}

FloorAgents are the agents whose session files time a call from what asked for it to its reply's end, never from inside it: Claude Code's and Claude Desktop's from the line that asked, written before it was sent; Codex's call in a turn of several from what the model had last been given before the call's first output item, to its last (sessions' codexTiming). Their calls' tokens over that time are a floor of how fast the reply wrote. OpenCode's isn't checked against real sessions, so it tells none. app.js's LED_FLOOR_AGENTS has the same.

View Source
var LogCalls func(time.Time) []sessions.Call

LogCalls reads the calls the agents' own session files record, which the ledger adds to those the gateway logged. A variable for the tests.

View Source
var Periods = []Period{Today, Week, Month, All}

Periods lists the windows in order.

View Source
var PluginAgents func() []Known

PluginAgents lists the agents plugins add, which come and go as plugins do; package agent sets it.

Functions

func AgentOf

func AgentOf(ua string) string

AgentOf names the agent behind a client User-Agent. Known agents map to their magpie id; anything else keeps its product name.

func Append

func Append(r Record)

Append writes one record. Errors are swallowed: accounting must never break a call.

func CodexModelEffort added in v0.1.1238

func CodexModelEffort(p Period) map[string][]sessions.ModelEffort

CodexModelEffort reports each Codex thread's own priced ledger consumption.

func Computer added in v0.1.653

func Computer() (id, name string)

Computer is this computer's id and name, the id made the first time and kept in usage-computer.json, which no backup or sync carries.

func DecodeOf added in v0.1.1088

func DecodeOf(out, reasoning int, ms, ttft, firstText, flow int64, firstReasoning ...int64) (tokens int, w int64)

DecodeOf is what a reply's speed is told by: the tokens it was seen to write and the ms it took (tony on Discord: gpt-6.1-sol read 163 tok/s, with a 14 s first token). A reply that reasoned counts only its answer — its output less the reasoning, from its first text — as most vendors keep the reasoning out of the stream (OpenAI's is encrypted, its summary sent when it's done; Claude's may come with no text): its tokens were written before the first content, over time the window from it doesn't hold, and counting them read a 2,000-token think and a 200-token answer in 1.5 s as 1,467 tok/s. Its time goes to the wait before the answer. With no answer text, paced visible reasoning can time all output from firstReasoning. Missing evidence (including older records), hidden reasoning and an unknown flow leave the speed untold. The window is DecodeWindow's: 0 for a reply that tells none.

A reply its vendor held back and let go in a burst at its end tells none either (John on Discord: Kimi Code answering OpenCode read 1,367, then 2,237 and 2,012 tok/s). The burst is told two ways, as the reply's text came before it or with it:

  • flow, when known (Record.Flow), is how long its content took to come from where its window starts. A window HeldShare times longer had its content come in a small part of it: the rest was a wait while the vendor wrote and held it. The time it took to come is not the time it took to write (a burst let go over 236 ms read 528 tokens at 2,237 tok/s), and that time is not seen, so it tells no speed. A stream that came as it was written has a flow about its window.
  • a reply that reasoned is timed from its first text, its reasoning written before. When the answer would have been written over ThinkPace times faster than its reasoning came before it, from the first content to the first text, its first text came with the burst and the wait before it was the answer being written and held (22 reasoning tokens over 10.5 s, then 528 answer tokens in 236 ms). A reasoning hidden from the stream (OpenAI's, Claude's omitted) comes before the first content and leaves this time short; one that streams comes at the pace its answer does.

func DecodeWindow added in v0.1.811

func DecodeWindow(out int, ms, ttft int64) int64

DecodeWindow is the ms a reply of out tokens, ms long with its first content at ttft, took to write them: 0 when it wasn't timed, wrote nothing, or came too fast to tell a speed by (above).

func DropAllShared added in v0.1.653

func DropAllShared()

DropAllShared lets every other computer's usage go: sync, or its sharing of usage, was turned off.

func DropShared added in v0.1.653

func DropShared(computer, day string)

DropShared lets another computer's day go, and its folder with its last.

func FormatCost added in v0.1.404

func FormatCost(amountUSD float64, currency string, rate float64) string

FormatCost renders an effective-price cost, kept in USD everywhere it's stored, as the CLI and TUI show it: at amountUSD's own price when currency isn't "cny", else converted at rate (CNY per one USD, from internal/fx; a rate of 0 or below also falls back to USD, a stale or missing rate being no reason to hide the number). Whole dollars or yuan above 100, cents above 1, else thousandths, so a fraction of a cent still shows as something.

func GatewaySessionWindow added in v0.1.1146

func GatewaySessionWindow(since time.Time) ([]GatewaySession, []GatewayDayUsage)

GatewaySessionWindow returns gateway sessions and their daily usage for a local-date range. It is kept separate from the GUI conversion so callers can use the same single usage-log snapshot for list and stats.

func GatewaySessionWindowExcept added in v0.1.1146

func GatewaySessionWindowExcept(since time.Time, nativeKeys map[string]bool) ([]GatewaySession, []GatewayDayUsage)

func GeminiServing added in v0.1.1113

func GeminiServing(sent, served string) bool

GeminiServing reports whether served is the name Google's backend gives the deployment that serves the Gemini model sent: the model's own name with "-n" after it. Antigravity answers every level of gemini-3.8-flash (-low, -medium, -high) with modelVersion gemini-3.8-flash-n, an id no request can ask for (404) and fetchAvailableModels never lists, so it is that model, not another one swapped in (dumplings on Discord). It holds for Gemini models only: gpt-5 answered as gpt-5-n is still a swap, as is gemini-3.8-flash answered as gemini-3.8-flash-lite or gemini-3.7-flash-n.

func GrokServing added in v0.1.1159

func GrokServing(sent, served string) bool

GrokServing reports whether served is xAI's name for the build serving the Grok model sent: the model's own name with "-build" after its version. xAI answers grok-4.7 as grok-4.7-build, and grok-4.6 as grok-4.6-build (#1455, cybershape; two relays' logs say the same), and its own list (SuperGrok's, 2026-10-02) offers grok-4.7 and its fast variant grok-4.7-build-fast, never a grok-4.7-build to ask for. It holds for Grok models only, and for the same variant: grok-4.7 answered as grok-4.7-build-fast (the fast one), grok-4.6-build or grok-4.7-mini is still a swap, as is gpt-5 answered as gpt-5-build.

func GroupRouted added in v0.1.563

func GroupRouted(sent, served string) bool

GroupRouted reports whether sent is a routing group of another magpie (a remote magpie's "group/<id>", the one name only a magpie answers to) and served the model its reply named: the member the group routed the request to, which is the group doing its job, not the vendor serving another model than asked (莫 on Discord: group/auto-… answered by deepseek/deepseek-v4.1-flash, marked a swap).

func HistoricalWindowHolds added in v0.1.1258

func HistoricalWindowHolds(cards []provider.SubscriptionQuota, hist []provider.QuotaHistory, from, to time.Time) []provider.SubscriptionQuota

HistoricalWindowHolds reckons each window at its last reading in [from, to), not from today's percentage. cards supply the window's scope, never its current figures. Each returned card has one window, because the windows may have been sampled at different times. A shorter window is its last observed cycle, not all its cycles added.

func IngestReported added in v0.1.1220

func IngestReported(recs []Record) ([]bool, error)

IngestReported writes the calls a client reported (#1874) to the log the gateway writes its own to, under the same lock, each marked SourceReported. A call whose request id the log already has as reported, or that came earlier in recs, is skipped. It answers, for each record, whether it was written. The records must already be checked: each has a RequestID.

Nothing else the gateway does on a call happens: no observer sees it (the call is the client's, which can send its own), and no caller key's budget is spent, since the call didn't go through magpie.

func IsAgent added in v0.1.1196

func IsAgent(id string) bool

IsAgent says whether id is an agent magpie knows, its own or a plugin's, rather than the product name of some other client (Python-urllib).

func KeepShared added in v0.1.653

func KeepShared(d SharedDay) error

KeepShared keeps another computer's day here, over the one kept before.

func LastSeen added in v0.1.91

func LastSeen(agent string) time.Time

LastSeen is when a request from the agent last reached the gateway: this process's own, else the newest in the log's last stretch. Zero if none.

func Ledger added in v0.1.408

func Ledger(p Period, f Filter) (rows []Row, sum Totals, agents []string)

Ledger is LedgerOf's rows, sum and agents.

A row's swapped is judged by the upstream names in force now, not by the one its call went out under: the names are read from the settings here, once, so a name given or changed since a call was recorded re-judges that call — one recorded under the name magpie knows the model by, and read after a name was set for it, reads as a swap although the vendor answered with the very model that was asked for. A record keeps the canonical id and the name the vendor's own reply gave (served_model) and not the name the request was sent under, so this is the only name there is to judge by — read as the name that went out, which the effort a record keeps of makes exact: an Antigravity account is asked for the variant of the model that level picks, so a reply naming that one is the model that was asked for and not another, whatever the family it is a level of. It is how ledgerWith's loop already treats the rest of a row: a provider renamed since is put back to the id it has now, and the names in force now are what the catalog lists the models under as well.

Storing the name that was sent would judge each call by what it was really sent under, and it is the better answer, but it is not this function's to make: it is a field on every record the gateway writes, and so a change to the file's format — a column a row written by an older magpie has no value for, and a decision about whether an empty one means "no upstream name" or "unknown" — rather than a row of a report. The rows here are what a reader sets beside a bill, and a name in force today is the one whose reply a bill's model column is read against.

The names are read here, once for the lot, rather than asked for a row at a time, so a caller already holding them — a report over the provider/model it was given — reads them the same way and reaches the same judgement: what a row says is the same however the rows were asked for.

func ModelAtKey added in v0.1.996

func ModelAtKey(provider, model string) string

ModelAtKey is a call's "modelAt": its provider's id and its model, as "provider/model" (a provider's id has no "/").

func NewCoster added in v0.1.675

func NewCoster() func(Record) (float64, bool)

NewCoster prices one record at a time at the effective prices the ledger uses, read once: its cost in USD, and false for a call with tokens but no known price.

func NewPricer added in v0.1.591

func NewPricer() func([]Record) Totals

NewPricer totals requests' token-bearing attempts at one snapshot of the effective prices used by the ledger. Reuse it for one page of routes; make it again on the next read so a changed tariff re-prices history.

func Observe added in v0.1.1243

func Observe(o Observer) (remove func())

Observe registers o until remove is called. While this process serves the gateway it is started at once.

func OthersDir added in v0.1.653

func OthersDir() string

OthersDir is where the other computers' days are kept: one folder each.

func OwnDays added in v0.1.653

func OwnDays(since time.Time) map[string][]SharedCall

OwnDays are this computer's calls since the day of since, by day, as the Usage page lists them: a call another magpie passed on here is left to that computer, which counts it itself, and where the request archive keeps a call is this computer's own.

func Path

func Path() string

Path is the log file: ~/.config/magpie/usage.jsonl (XDG-aware).

func Pricer added in v0.1.1243

func Pricer() func(Record) *catalog.Price

Pricer uses the same effective price as the model CLI and session totals: explicit provider/model or provider-wide prices (including zero), then the provider catalog, then the maker catalog. Settings are read once per query.

A model no price is known for is priced as the model the vendor's reply said answered (Served), at the same provider, when that one has a price: a relay that sells kimi-k3 as moonshot-kimi-k3 answers as kimi-k3, which is listed (#1498). The model asked for keeps its own price whenever it has one, a zero set by the user included, so a ledger that matched the vendor's bill still does.

func PurposeKinds added in v0.1.969

func PurposeKinds() map[string]PurposeKind

PurposeKinds returns a copy of the catalog used to generate the UI's catalog. Add aliases here, then run go generate ./internal/usage from the repo root.

func PurposeOf added in v0.1.854

func PurposeOf(kind string) string

PurposeOf groups the agent's equivalent call-kind spellings for filtering. The recorded Kind stays untouched. Prefixing nonempty kinds keeps future names distinct from the unmarked bucket (which is not necessarily a chat).

func RelayRenamed added in v0.1.1166

func RelayRenamed(sent, served string) bool

RelayRenamed reports whether sent is served's own name with a relay's word in front of it: a relay that sells kimi-k3 as moonshot-kimi-k3 is answered as kimi-k3 (#1498, liuweifeng), one that sells deepseek-v4.1-flash as claude-deepseek-v4.1-flash (#1383) as deepseek-v4.1-flash. What is left must name a version, so a bare word (flash, mini) is never taken for the model sent.

func ReportedAs added in v0.1.1227

func ReportedAs(ua string) string

ReportedAs is the agent the Usage and Sessions pages count a client's calls under: AgentOf's, unless the user counts that client as an agent of theirs (settings.ClientAgents). Only for what is reported: the gateway's own choices (routing, keys, caps) go by AgentOf, the client's own name.

func SameSpelled added in v0.1.958

func SameSpelled(sent, served string) bool

SameSpelled reports whether served is sent spelled with other separators or case: a row or route kept marked swapped before Swapped knew it is read as not swapped.

func Saw added in v0.1.91

func Saw(agent string)

Saw notes a request from an agent arriving now.

func ServeObservers added in v0.1.1243

func ServeObservers() (stop func())

ServeObservers starts every observer registered, and each one registered later, until stop: the gateway calls it as it starts serving.

func ServingName added in v0.1.1159

func ServingName(sent, served string) bool

ServingName reports whether served is the vendor's own name for the deployment serving the model sent, which no request asks for by: Google's for a Gemini model (GeminiServing), xAI's for a Grok one (GrokServing).

func SharedKept added in v0.1.653

func SharedKept() []string

SharedKept are the other computers' days kept here, as "<id>/<day>".

func SourceOf added in v0.1.1104

func SourceOf(r Row) string

SourceOf is the source a row is of, as the page's cells name them: the calls magpie's gateway served and the ones read from the agents' session files. A call the gateway turned away itself is neither — the page says under its totals that local rejections are not counted — so it is "" and picking either cell leaves it out, as the cells' own counts already do.

func Summaries added in v0.1.1093

func Summaries(p Period) (Summary, Summary)

Summaries is Summarize(p) and Direct(p) asked at one moment, for a page that shows the two together: asked one after the other, midnight could fall between them and set one day's calls through magpie beside the next day's calls not through it. The gateway's sessions are its top summaryCacheSessions (SummarizeTop), of its SessionCount: a page lists a few, and the summary of a long history is then kept rather than worked out again.

func Swapped added in v0.1.408

func Swapped(sent, served string) bool

Swapped reports whether served is another model than sent: not the same name, however dated, pinned or prefixed. A call that went out as one level of a model (an Antigravity account's gemini-3.8-flash-medium) answered under the model's own name is that model at that level: Antigravity's reply names the family, not the variant (#462). Another level is another model, as AntigravitySentID has it. A vendor's "auto" (Copilot's, Cursor's) asked it to pick, so whichever answers wasn't swapped in, nor is the member another magpie's routing group sent it to (GroupRouted), nor a vendor's own name for the deployment serving the model (ServingName), nor the model a relay sold under a name of its own (RelayRenamed).

func ValidComputer added in v0.1.653

func ValidComputer(id string) bool

ValidComputer is whether id can be another computer's id: a folder name and a file name's first part, nothing else.

func Vias added in v0.1.284

func Vias(since time.Time) map[string][]Via

Vias is what the gateway sent each session's calls to since a time, by agent id and the session's id ("codex|<id>"), the most calls first.

func Visit added in v0.1.675

func Visit(since time.Time, fn func(Record))

Visit calls fn with each record from since on, as the ledger's index reads usage.jsonl: only its blocks that reach since are decoded.

func WithWindowHolds added in v0.1.1120

func WithWindowHolds(qs []provider.SubscriptionQuota, now time.Time) []provider.SubscriptionQuota

WithWindowHolds is qs with each window's Holds reckoned from the log, as it stood at now. qs itself, which the caller may share, is left as it is.

func WriteCSV added in v0.1.408

func WriteCSV(w io.Writer, rows []Row) error

WriteCSV writes rows as CSV, a header first (cache_write_tokens is every cache write, as it always was, and the last two columns split it into 5-minute and 1-hour writes): times in RFC 3339 with their offset, the cost in USD at the effective price (empty when unknown), error "true" for a call that failed: answered with a status of 400 or more, or ended by an error a session file tells. tries is how many tries the gateway made at the request the row ends, when more than one, and handed_on "true" for the row of a try that failed and handed the request on: the request's outcome is the row with the same route_id after it. tier_asked, tier_sent and tier_served are the service tier the client asked for, the one sent on the row's try and the one the reply said it ran at, each empty when there was none (Record.TierAsked).

Types

type Bodies added in v0.1.1243

type Bodies int

Bodies is how much of a request's and reply's bodies an observer wants.

const (
	NoBodies       Bodies = iota
	CapturedBodies        // the first 256 KiB Recent calls keeps
	WholeBodies           // uncut: the reply is spooled as it streams
)

func ObservedBodies added in v0.1.1243

func ObservedBodies() Bodies

ObservedBodies is the most of a request's bodies any observer wants now.

type Day added in v0.1.1134

type Day struct {
	Date   string  `json:"date"` // YYYY-MM-DD
	Calls  int     `json:"calls"`
	Tokens int     `json:"tokens"` // in, out and through the cache, as the chart counts them
	Cost   float64 `json:"cost"`   // of the priced calls, USD
	// Unpriced: calls with tokens but no known price, not in Cost
	Unpriced int `json:"unpriced,omitempty"`
}

Day is one day's calls, by the local calendar day they fell on.

type Filter added in v0.1.408

type Filter struct {
	Day       string // YYYY-MM-DD in the chart's local time zone; "" is the whole period
	CallerKey string
	RouteID   int64
	Model     string // exact model selected in the ranking
	Agent     string
	Provider  string // a provider's id, as the ledger's rows have it
	Purpose   string // PurposeOf's key; empty keeps every purpose
	// Account narrows to the calls a subscription account answered, by its
	// name as Record.Account gives it (#557)
	Account string
	// Failed keeps the rows of the requests the agent got an error for
	// (Record.LostRequest), Retried those of the requests answered after a
	// try that failed first (Record.RetriedRequest): what the Requests
	// card's two counts are of
	Failed  bool
	Retried bool
	Query   string
	// Computer narrows to the calls of one computer (#542): ThisComputer,
	// OtherComputers, or another's id; "" is every computer's
	Computer string
	// Through narrows to one of the sources a row can be of
	// (SourceGateway, SourceSession, SourceReported); "" keeps all. It is what the requests
	// page's source cells filter by, and, as with the other dimensions, the
	// cells themselves are counted with it cleared so all three stay
	// switchable. Not Record.Via, which is the computer that forwarded the
	// call (#542).
	Through string
}

Filter narrows the ledger: to a route, one agent (its id, as AgentOf gives it), to one provider, to the failed calls, and to the rows whose models, provider, host or session hold Query (any case).

type GatewayDayUsage added in v0.1.1146

type GatewayDayUsage struct {
	Date, Agent, Model string
	sessions.Tokens
	Cost   float64
	Priced bool
}

type GatewayModel added in v0.1.1146

type GatewayModel struct {
	Model string `json:"model"`
	sessions.Tokens
	Cost   float64 `json:"cost"`
	Priced bool    `json:"priced"`
}

type GatewaySession added in v0.1.1146

type GatewaySession struct {
	Agent  string            `json:"agent"`
	ID     string            `json:"id"`
	Start  time.Time         `json:"start"`
	Last   time.Time         `json:"last"`
	Models []GatewayModel    `json:"models"`
	Daily  []GatewayDayUsage `json:"-"`
	sessions.Tokens
	Cost     float64 `json:"cost"`
	Unpriced int     `json:"unpriced"`
}

GatewaySession is a read-only projection of requests served by this gateway. Its identity is the agent name and the session id supplied by the client. Unidentified requests and generated recording identities are left out.

func GatewaySessionByID added in v0.1.1146

func GatewaySessionByID(agent, id string, nativeKeys map[string]bool) (GatewaySession, bool)

func GatewaySessions added in v0.1.1146

func GatewaySessions(since time.Time, nativeKeys map[string]bool) []GatewaySession

GatewaySessions aggregates one usage snapshot without reading routing history or agent files. NativeKeys contains identities already represented by local session files and is used to suppress the duplicate projection.

type Group

type Group struct {
	ID              string `json:"id"`
	Provider        string `json:"provider,omitempty"` // models only
	Model           string `json:"model,omitempty"`
	ProviderKeyID   string `json:"providerKeyId,omitempty"`
	ProviderKeyName string `json:"providerKeyName,omitempty"`
	// Account is the subscription account of an Accounts group: "" for
	// the calls of an account's provider whose record names none
	Account       string `json:"account,omitempty"`
	CallerKeyID   string `json:"callerKeyId,omitempty"`
	CallerKeyName string `json:"callerKeyName,omitempty"`
	// Host is where the calls went, when the provider's id has gone to
	// more than one place, or elsewhere than the provider goes now: its
	// calls are then told apart by it, not summed under the id.
	Host string `json:"host,omitempty"`
	// Agent is the agent a session is of (sessions only).
	Agent string `json:"agent,omitempty"`
	Totals
	// Own is the share of these calls magpie made by itself (Record.ByMagpie):
	// a routing classifier's, a web search's or an image description's,
	// counted in Totals too, since they went to the same key or account
	// (#1839). Filled for models, keys and accounts; a value, so a
	// summary's copies share nothing.
	Own OwnShare `json:"own,omitzero"`
}

Group is the share of one agent or model.

type Heatmap added in v0.1.1134

type Heatmap struct {
	From string `json:"from"`
	To   string `json:"to"`
	Days []Day  `json:"days"`
}

Heatmap is the days from From to To (today), both YYYY-MM-DD in the local time zone: only those with calls are listed, oldest first.

func HeatmapOf added in v0.1.1134

func HeatmapOf(f Filter) Heatmap

HeatmapOf is the last HeatmapWeeks weeks of the calls the filter keeps, a day each. The filter's Day is not one of its dimensions here: the heatmap is every day's.

type Known added in v0.1.43

type Known struct {
	ID    string
	Names []string // its id and aliases, which a record may already carry
	UA    []string // what its User-Agent begins with
}

Known is an agent as its requests name it.

type Ledgered added in v0.1.579

type Ledgered struct {
	Rows      []Row
	Sum       Totals
	Agents    []string
	Providers []string
}

Ledgered is a period's calls that a filter keeps, newest first, with their sum, and the agents and providers that made any call in the period (their ids, for a filter to offer).

func LedgerOf added in v0.1.579

func LedgerOf(p Period, f Filter) Ledgered

LedgerOf is the calls of a period that the filter keeps. The gateway's log comes with the calls the agents' session files record that the gateway did not see.

func LedgerOfAt added in v0.1.1093

func LedgerOfAt(p Period, f Filter, now time.Time) Ledgered

LedgerOfAt is LedgerOf with the period read at now, for an answer that names that moment elsewhere too, as an exported CSV's name does.

func (Ledgered) Filtered added in v0.1.579

func (l Ledgered) Filtered(f Filter) Ledgered

Filtered narrows one snapshot without reloading either log or repricing rows.

type Observer added in v0.1.1243

type Observer interface {
	// Serve runs it while this process serves the gateway; stop ends it,
	// after the calls in flight have finished
	Serve() (stop func())
	// Bodies is how much of a request's bodies it wants now
	Bodies() Bodies
	// Begin is asked as each gateway request starts; nil leaves the
	// request unwatched by it
	Begin(Request) Watch
	// Ledger is each row Append writes. A row of a request some observer
	// watched has Watched set.
	Ledger(Record)
}

Observer is the gateway's observer point (P3 of docs/subsystems/plugin-runtime.md): it watches what the gateway serves and what the ledger is written, and never changes either. A plugin registers one with Observe while it is on (plugins/otel registers internal/otlp's exporter); with none registered the gateway does no observing work at all, not even keeping a reply's whole body.

It lives here, not in the gateway, because every row it sees is a usage Record and the ledger's other writers (budget, routing decisions) reach it too.

func Observers added in v0.1.1243

func Observers() []Observer

Observers are the observers registered now.

type OwnShare added in v0.1.1216

type OwnShare struct {
	Calls  int     `json:"calls"`
	Tokens int     `json:"tokens"` // in and out, cache aside, as Totals.Tokens
	Cost   float64 `json:"cost"`
}

OwnShare is what magpie's own calls of a group came to.

type Part added in v0.1.579

type Part struct {
	Calls  int     `json:"calls"`
	Tokens int     `json:"tokens"`
	Cost   float64 `json:"cost"`
	// how fast its replies came, as Totals has it: a chart of speed says
	// each one's at each point (#860)
	Timed     int   `json:"timed,omitempty"`
	TTFT      int64 `json:"ttft_ms,omitempty"`
	DecodeMs  int64 `json:"decode_ms,omitempty"`
	DecodeOut int   `json:"decode_out,omitempty"`
}

Part is what one provider, agent or model had of a point of the timeline.

type Period

type Period string

Period is a window of time to sum over.

const (
	Today Period = "today"
	Week  Period = "7d"
	Month Period = "30d"
	All   Period = "all"
)

func LastOf added in v0.1.1233

func LastOf(d time.Duration) (Period, bool)

LastOf is the rolling window of d ending now; ok is false when d is not positive.

func RangeOf added in v0.1.1162

func RangeOf(from, to string) (Period, bool)

RangeOf is the period of the days from one date (YYYY-MM-DD) to another, in either order; ok is false when either is not a date.

func (Period) IsRange added in v0.1.1162

func (p Period) IsRange() bool

IsRange is whether p is days the reader picked rather than a preset.

func (Period) Known added in v0.1.1162

func (p Period) Known() bool

Known is whether p is a period the usage pages answer for: a preset, a picked range or a rolling window.

func (Period) Last added in v0.1.1233

func (p Period) Last() time.Duration

Last is the length of a rolling window ("last:24h"), 0 when p is not one.

func (Period) Since added in v0.1.408

func (p Period) Since(now time.Time) time.Time

Since is when the period began, as of now; zero for all.

func (Period) Until added in v0.1.1162

func (p Period) Until(now time.Time) time.Time

Until is when the period ends, as of now: the midnight after a picked range's last day, zero for a preset, which runs up to now.

type Point

type Point struct {
	Label string    `json:"label"`
	Time  time.Time `json:"time"`
	Totals
}

Point is one bar of the timeline.

type PurposeKind added in v0.1.969

type PurposeKind struct {
	Purpose string `json:"purpose"`
	Name    string `json:"name"`
}

PurposeKind is the shared filtering key and untranslated display name of a kind.

type Record

type Record struct {
	Operation       string    `json:"operation,omitempty"`
	RouteID         int64     `json:"route_id,omitempty"` // the gateway Route, shared by its attempts
	Time            time.Time `json:"t"`
	Agent           string    `json:"agent"` // magpie agent id, or the client's product name
	Provider        string    `json:"provider"`
	Host            string    `json:"host,omitempty"`          // where the call went: provider.Where then
	ProviderKeyID   string    `json:"providerKeyId,omitempty"` // fingerprint of the API key actually used
	ProviderKeyName string    `json:"providerKeyName,omitempty"`
	// ProviderAccount is the subscription account that answered the call,
	// as the Routing trace names it (provider.Account.User: an email, a
	// login): the one that took over after a failover, the one pinned by
	// X-Magpie-Account. Never a token. "" for a key or a provider without
	// an account, and in a record written before it was kept (#557).
	ProviderAccount string `json:"providerAccount,omitempty"`
	CallerKeyID     string `json:"callerKeyId,omitempty"`
	CallerKeyName   string `json:"callerKeyName,omitempty"`
	// SessionProvider is the provider ID recorded by the agent. SessionAccount
	// identifies the session's creator, not the account used for an API request.
	// Neither field establishes an upstream route from today's configuration.
	SessionProvider string `json:"session_provider,omitempty"`
	SessionAccount  string `json:"session_account,omitempty"`
	// SessionOfficialLogin marks a matched account's explicit official login
	// metadata, not this call's endpoint or billing authentication.
	SessionOfficialLogin bool   `json:"session_official_login,omitempty"`
	Model                string `json:"model"` // the provider's model id
	// Requested is the model id the agent asked for (a magpie alias, a
	// routing group, provider/model…), and Served the model the vendor's
	// reply says answered, when it named one: a ledger to set beside the
	// vendor's own bill. Neither is in a record written before they were.
	Requested string `json:"req,omitempty"`
	Served    string `json:"served,omitempty"`
	// Upstream: the provider an aggregator (OpenRouter …) said answered
	// behind it — DeepInfra, Novita — when its reply named one
	Upstream string `json:"upstream,omitempty"`
	Input    int    `json:"in"`
	Output   int    `json:"out"`
	// CacheRead is the prompt tokens read from the vendor's cache, and
	// CacheWrite those written to it (Anthropic's cache creation,
	// cache_creation_input_tokens); neither is in Input. Both are written
	// on every record, 0 too, as in and out are, so a call whose prompt
	// missed the cache reads as that rather than as one whose cache wasn't
	// kept (#1839). A record from before they were has neither: 0.
	CacheRead  int `json:"cache_read"`
	CacheWrite int `json:"cache_write"`
	// CacheWrite1h is how many of CacheWrite were written to be kept for
	// an hour (Anthropic's 1-hour TTL, billed at 2× input where 5 minutes'
	// is 1.25×); the rest were for 5 minutes. A record from before it was
	// kept has none: all its writes are counted as 5-minute ones, as they
	// were.
	CacheWrite1h int `json:"cache_write_1h,omitempty"`
	Reasoning    int `json:"reasoning,omitempty"`
	// Effort is the reasoning the model was asked for — a routing group's
	// pick for the turn, or the agent's own — as it takes it; "" for none
	Effort string `json:"effort,omitempty"`
	// The service tier, as OpenAI's service_tier words it ("priority",
	// "ultrafast", "flex", "default"; "fast" for Claude's speed "fast"):
	// TierAsked what the client's request asked for, TierSent what went
	// out to the vendor on this try, after a fast pick or a group member
	// set Fast asked for priority and an account whose plan doesn't offer
	// Ultrafast was asked for priority in its place, and TierServed what
	// the vendor's reply said it ran at. Each is "" when there was none:
	// TierServed is never taken from what was asked or sent, only from a
	// reply that names it (#1784)
	TierAsked  string `json:"tier_asked,omitempty"`
	TierSent   string `json:"tier_sent,omitempty"`
	TierServed string `json:"tier_served,omitempty"`
	Millis     int64  `json:"ms"`
	// TTFT: ms from the request to its reply's first content — text,
	// reasoning or a tool call — and FirstText to its first text, counted
	// as Millis is, so Millis-TTFT is how long the reply took to write;
	// none for a reply that wasn't streamed (#196)
	TTFT      int64 `json:"ttft_ms,omitempty"`
	FirstText int64 `json:"first_text_ms,omitempty"`
	// Compaction is Codex's explicit native marker. Nil means no matching
	// native Codex call established whether a gateway row compacted.
	Compaction *bool `json:"compaction,omitempty"`
	// FirstReasoning is the start of paced visible reasoning, absent for hidden reasoning.
	FirstReasoning  int64 `json:"first_reasoning_ms,omitempty"`
	ReasoningHidden bool  `json:"reasoning_hidden,omitempty"`
	// Sent: ms from the request to its answering try going out to the
	// vendor, its body written, as TTFT is counted: magpie's own time and
	// the tries that failed first are before it, and TTFT-Sent is how
	// long the vendor took to its first content. 0 where it isn't known
	// (a reply not streamed, or a vendor magpie doesn't reach over HTTP).
	Sent int64 `json:"sent_ms,omitempty"`
	// Of Sent, what was magpie's: Own is magpie's own work on the request
	// (reading it, translating it, choosing whom to ask), Retried the
	// tries that failed first and the pauses after them, Queued the wait
	// for a slot of a key's or account's MaxConcurrency or MaxRPM; the
	// rest, Sent-Own-Retried-Queued, is connecting to the vendor and
	// sending it the body. #1532: a vendor that held a try ~100 s and
	// said 503 read as 104 s "In magpie". 0 in a record written before
	// they were, where Sent is all that is known.
	Own     int64 `json:"own_ms,omitempty"`
	Retried int64 `json:"retried_ms,omitempty"`
	Queued  int64 `json:"queued_ms,omitempty"`
	// Flow: the ms the reply's content took to come, from where its speed
	// is counted (DecodeOf): from a tenth of its content to nine tenths,
	// spread over the whole. A reply a vendor held and let go in one burst
	// has a flow of next to none, however long its end came after its
	// first content, and so tells no speed (John on Discord: a Kimi Code
	// reply read 1,367 tok/s). 0 where it isn't known.
	Flow   int64 `json:"flow_ms,omitempty"`
	Status int   `json:"status"`
	// Error is why a call failed, in the vendor's words and cut short;
	// ErrType what its body called the error (rate_limit_error,
	// usage_limit_reached); RequestID the id the vendor gave the call; and
	// Endpoint the path the agent called, with the one it was sent to when
	// it went out in another protocol (/v1/messages → /v1/chat/completions)
	Error     string `json:"err,omitempty"`
	ErrType   string `json:"err_type,omitempty"`
	RequestID string `json:"rid,omitempty"`
	// ResponseID is the response object ID actually sent to the client.
	ResponseID string `json:"response_id,omitempty"`
	Endpoint   string `json:"ep,omitempty"`
	// Stop is why the upstream said its reply ended, in its own words
	// (stop_reason, finish_reason …): "" when it said none, a stream that
	// just stopped. Told apart by it, a reply that ended too soon is the
	// upstream's own end_turn or a cut (蓝猫 on Discord).
	Stop string `json:"stop,omitempty"`
	// Session is the conversation the call was part of, as its agent names
	// it (X-Magpie-Session, or the session header Claude Code, Codex or
	// OpenCode sends): several sessions on one model told apart
	Session string `json:"session,omitempty"`
	// NativeSession retains the client header when X-Magpie-Session overrides it.
	NativeSession string `json:"native_session,omitempty"`
	// Subagent is the subagent that made the call, as its agent names it
	// (Claude Code's x-claude-code-agent-id, the agentId of its
	// subagents/agent-<id>.jsonl): Session stays the conversation it was
	// spawned in, so its calls are counted there too. ParentAgent is the
	// subagent that spawned it in turn (x-claude-code-parent-agent-id), ""
	// when the conversation itself did or the agent doesn't say.
	Subagent    string `json:"subagent,omitempty"`
	ParentAgent string `json:"parent_agent,omitempty"`
	// Rejected is a request refused locally before an upstream was contacted.
	Rejected bool `json:"rejected,omitempty"`
	// Tries is how many tries the gateway made at the request this row
	// ends, as its route lists them, when there was more than one: a
	// request answered on a later try was answered, not failed (小一 on
	// Discord). HandedOn marks the row of one try that failed and handed
	// the request on to the next (a vendor's refusal kept for what it
	// billed): it is not the request's outcome, which a later row with
	// the same RouteID is, so it is neither a request nor a failure of
	// its own. Neither is in a record written before they were; the log
	// index tells HandedOn from such a record's route (handedOn).
	Tries    int  `json:"tries,omitempty"`
	HandedOn bool `json:"handed_on,omitempty"`
	// Kind is what the agent made the call for when it isn't a turn of
	// the conversation: a Codex subagent's (review, compact, guardian…)
	Kind string `json:"kind,omitempty"`
	// Via is the computer whose magpie passed the call on to this one (a
	// Remote magpie provider there), Agent being the agent's on it; "" when
	// no other magpie forwarded it (including direct LAN/container clients).
	Via string `json:"via,omitempty"`
	// Archive is where the request archive keeps the call, "<date>/<id>"
	// (gateway/archive.go), when it was on: the Usage page reads it back
	// by it long after Recent calls has let the call go (#447)
	Archive string `json:"archive,omitempty"`
	// Computer is the other computer the call was made on, by its id, for
	// a call brought here by sync (#542); "" for one made here, as every
	// call in usage.jsonl is
	Computer string `json:"computer,omitempty"`
	// Source is SourceReported for a call a client told magpie of through
	// POST /v1/magpie/usage/requests (#1874), one that went straight to its
	// vendor and that magpie never saw; "" for the gateway's own. Reported
	// calls count in the totals and are told apart, and filtered, on the
	// page.
	Source string `json:"source,omitempty"`
	// OwnCost is what such a client said the call cost, in USD. nil when it
	// said nothing: the call is then priced at magpie's own prices, as the
	// gateway's calls are, and priced again when those change.
	OwnCost *float64 `json:"cost,omitempty"`
	// Watched is a row of a request an Observer watched (observe.go): it
	// has the request's attempts already, so it can pass the row over.
	// Never persisted.
	Watched bool `json:"-"`
	// Local is true only for verified loopback gateway requests. It is used
	// for local-session deduplication, never persisted or exported.
	Local bool `json:"-"`
	// BodyIn and BodyOut are the request and reply as the gateway
	// captured them, filled only while an Observer wants them (an OTLP
	// export with bodies on, #538) and never written to usage.jsonl.
	BodyIn  string `json:"-"`
	BodyOut string `json:"-"`
}

Record is one call.

func Load

func Load(since time.Time) []Record

Load streams complete records from the log. Historical request bodies are not retained by a global cache after the caller finishes with this snapshot.

func (Record) Account added in v0.1.660

func (r Record) Account() string

Account is the subscription account that answered the call: the one the record names, or, in a record written before it named one, the account its Host said the call went out as then ("chatgpt.com as dee@example.com") — what the record itself says, never today's sign-in. "" when neither tells (#557).

func (Record) ByMagpie added in v0.1.1216

func (r Record) ByMagpie() bool

ByMagpie is whether magpie made the call by itself rather than an agent: its routing classifier, System One's decision, a web search or an image description it asked for (#1839).

func (Record) CostAt added in v0.1.1032

func (r Record) CostAt(p catalog.Price) float64

CostAt is what the call costs at a price: at the tier its input reaches, its 1-hour cache writes at theirs. A reported call that said what it cost costs that, at any price.

func (Record) Decode added in v0.1.1088

func (r Record) Decode() (tokens int, w int64)

Decode is the record's DecodeOf.

func (Record) Failed added in v0.1.579

func (r Record) Failed() bool

Failed is whether the call failed: the agent was answered an error status, or, for a call read from a session file, which has no status, the file says an error ended it.

func (Record) IsRejected added in v0.1.579

func (r Record) IsRejected() bool

IsRejected also recognizes local rejections written before the explicit flag.

func (Record) IsReported added in v0.1.1220

func (r Record) IsReported() bool

IsReported is a call an app reported itself (#1874), which never went through magpie. It is shown and counted on the Usage page only: nothing that acts on live traffic (a key's budget, a window's holds, the Sessions page's gateway sessions, what a session was routed to, Codex's sealed answers or remembered model) reads it, since it may be a backfill of calls long past and none of them were magpie's to route.

func (Record) LostRequest added in v0.1.1182

func (r Record) LostRequest() bool

LostRequest is whether r ends a request the agent got an error for: a failed row that no later try took over from.

func (Record) RetriedRequest added in v0.1.1182

func (r Record) RetriedRequest() bool

RetriedRequest is whether r ends a request that was answered after a try that failed first.

type Request added in v0.1.1243

type Request struct {
	Header http.Header
	// Agent the agent that sent it, Session its native session when it
	// names one, else magpie's
	Agent, Session string
	// Kind its call kind ("web_search", "vision", …), "auxiliary" for
	// Claude's tool-less small helper calls, "" for an agent's own turn
	Kind string
	// Plain is a loopback request with no gateway key, not passed on from
	// another magpie: one the agent's own transcript can hold
	Plain bool
}

Request is what an Observer is told of a request as it starts.

type RequestPage added in v0.1.579

type RequestPage struct {
	Rows              []Row
	Sum               Totals
	Total             int
	Agents, Providers []string
	Purposes          []string // all purposes in the period, independent of the selected filter
	CallerKeys        []Group
	// Through and Direct are the rows the gateway served and the rows read
	// from the agents' own session files, which the gateway never saw. Sum
	// is the two together; the page shows the three beside each other so a
	// reader can tell what magpie carried from what it only read about.
	Through, Direct Totals
	// Reported are the calls a client told magpie of (#1874), which went
	// straight to their vendor: counted in Sum, apart from both of those.
	Reported Totals
	// Accounts are the subscription accounts that answered calls in the
	// period, by provider and account, for the Account filter (#557)
	Accounts []Group
	Bucket   string
	Series   []SeriesPoint
	By       map[string][]Share
	ChartBy  map[string][]Share // the whole period's legend while a day is selected
	// Computers are the rows told apart by the computer they were made on,
	// ThisComputer's and each other's by id, of the rows without the
	// filter's computer, and Names what the others are called: none when no
	// other computer's calls were brought here by sync (#542)
	Computers []Share
	Names     map[string]string
	// Heat is the rows by day, as the chart's filter keeps them: only for
	// the heatmap's own period (HeatmapOf)
	Heat *Heatmap
}

func QueryPage added in v0.1.579

func QueryPage(p Period, f Filter, offset, limit int) RequestPage

type Row added in v0.1.408

type Row struct {
	Record
	// SessionRole is the query-time Codex actor classification. Unknown is
	// explicit so a shared session header cannot make an unpaired call look
	// like its root thread.
	SessionRole string `json:"session_role,omitempty"`

	// Cost is the call's effective price in USD, when its model's is known
	// (Priced); Swapped: the reply named another model than Model
	Cost    float64 `json:"cost"`
	Priced  bool    `json:"priced"`
	Swapped bool    `json:"swapped,omitempty"`
	// Routed marks a remote magpie group answered by one of its members.
	Routed bool `json:"routed,omitempty"`
	// Source is "log" for a call read from an agent's own session file,
	// which the gateway never saw: no status, no provider, no model asked
	// for, but what the file says of the call. "" for one the gateway logged.
	Source string `json:"source,omitempty"`
	// Client is the client's own name when the user counts it as another
	// agent (settings.ClientAgents, #1958): Agent is the one it is
	// counted as, this what the ledger recorded it as
	Client string `json:"client,omitempty"`
	// contains filtered or unexported fields
}

Row is one call as the ledger lists it.

func (Row) Decode added in v0.1.1220

func (r Row) Decode() (tokens int, w int64)

Decode is the row's DecodeOf, a Codex turn of one call from its session file held to CodexWindowMs and CodexBodyTokens.

func (Row) Floor added in v0.1.1180

func (r Row) Floor() (tokens int, ms int64)

Floor is the tokens and ms a row read from a session file tells the least its reply wrote at by, when its speed wasn't measured. 0, 0 when it tells none. app.js's ledFloorSpeed is the same rule for one row.

Mostly it is its output over its whole time, the wait for the first token in it, for a reply of 1,000 tokens or more over a second or more (a shorter one's wait is too much of it to tell anything; against Claude Code's own stream a floor read 2-5% under for 1,400 tokens or more, 11-33% for a shorter one) and no faster than MaxDecodeSpeed. A count of tokens is what bounds the share the wait can be, not a duration: a 5 s reply can be 4 s of wait (Fallmonkey on Discord asked for 5 s instead).

A Codex turn of one call that reasoned has its first token from the turn's end, but not its first text, so DecodeOf tells no speed. Its answer, its output less the reasoning, was all written after the first token, so the answer over the time from the first token to the end is a floor of the answer's speed, the reasoning written after the first token counted in its time. Its output over that time, as Fallmonkey asked, would count the reasoning written before the first token too and read high: a gateway reply of 496 tokens, 224 of them reasoning, 827 ms after a 10.2 s first token, reads 600 tok/s so, its measured answer 415, and this 329. That window is held to what a measured one is (CodexWindowMs, CodexBodyTokens): 827 ms can be one push as well.

A Codex turn of one call that tells neither, its window after the first token too short or its body too small, tells its output over the whole turn, the wait for the first token in it, by the same rule as any other such floor: 1,000 tokens or more over a second or more. It is the same formula, so it has the same gate: under 1,000 tokens the wait is too much of the time to tell anything (Fallmonkey on Discord: 11 tokens in 2.75 s read ≥4 tok/s while an 815-token Claude Code reply told nothing).

type SeriesPoint added in v0.1.579

type SeriesPoint struct {
	Point
	By map[string]map[string]Part `json:"by"`
}

SeriesPoint is a point of the timeline with its calls told apart, by each dimension, for a chart to stack them: the models of one point, say.

func LedgerSeries added in v0.1.579

func LedgerSeries(p Period, rows []Row) (bucket string, pts []SeriesPoint)

LedgerSeries is the rows of a period, the ledger's or a filter's, summed by hour, day or week, for the chart over them. A row is of the point its time falls in; the rows are in any order.

type Share added in v0.1.579

type Share struct {
	ID string `json:"id"`
	Totals
}

Share is one provider's, agent's or model's part of some rows.

func Breakdown added in v0.1.579

func Breakdown(rows []Row, by string) []Share

Breakdown sums rows by a dimension, the one with the most tokens first (then by calls, then by name).

type SharedCall added in v0.1.653

type SharedCall struct {
	Record
	Source string `json:"source,omitempty"`
}

SharedCall is a call as the ledger has it: Source "log" for one read from an agent's session file.

type SharedDay added in v0.1.653

type SharedDay struct {
	Computer string       `json:"computer"` // its id
	Name     string       `json:"name"`     // what it is called, its host name
	Day      string       `json:"day"`      // 2006-01-02, the day where it is
	Calls    []SharedCall `json:"calls"`
}

SharedDay is one computer's calls of one day, as it puts them on the server.

type Summary

type Summary struct {
	Period Period    `json:"period"`
	Since  time.Time `json:"since"`
	Totals
	Bucket       string  `json:"bucket"` // hour | day | week
	Series       []Point `json:"series"`
	Agents       []Group `json:"agents"`
	Models       []Group `json:"models"`
	ProviderKeys []Group `json:"providerKeys"`
	// Accounts are the subscription accounts' calls, by provider and the
	// account that answered (#557)
	Accounts   []Group `json:"accounts"`
	CallerKeys []Group `json:"callerKeys"`
	// Sessions are the calls that named their session, by session, the
	// most tokens first: all of them, or the top few of SessionCount where
	// it was asked for that way (SummarizeTop).
	Sessions     []Group `json:"sessions"`
	SessionCount int     `json:"sessionCount"`
}

Summary is a period, summed up.

func Direct added in v0.1.869

func Direct(p Period) Summary

Direct is a period's calls the agents made on their own, which the gateway never saw: those their session files record (Codex, Claude Code, Claude Desktop…), summed up as Summarize sums the gateway's, by agent, model and account. The Usage page's Requests tab and the tray panel count them beside the gateway's; magpie usage and the TUI show them in a part of their own (Kumo31 on Discord: Codex used outside magpie was on the window's Usage tab but not in magpie usage).

func Summarize

func Summarize(p Period) Summary

Summarize caches the four periods over an indexed log snapshot. Callers receive their own result slices, without retaining historical Records.

func SummarizeTop added in v0.1.1174

func SummarizeTop(p Period, n int) Summary

SummarizeTop is Summarize with only the top n sessions by tokens (all of them for n < 0); SessionCount says how many the period has. It is what a page that shows a few of them or none asks for: a summary of more than summaryCacheSessions sessions is kept with those only, and Summarize, which gives every one, works it out again each time. Over a long history (Vantiboo1ean, ~750k requests in ~84k sessions) that was every refresh of the Usage page and the TUI, and the page was sent all 84k.

type Totals

type Totals struct {
	// Calls are the requests, Errors those the agent got an error for, and
	// Retried those answered after a try that failed first (Record.Tries):
	// a try handed on is in neither count, but its tokens are in the sums
	Calls   int `json:"calls"`
	Errors  int `json:"errors"`
	Retried int `json:"retried,omitempty"`
	// Loops are those of Errors magpie ended for looping (LoopErrType):
	// a model's count of them says how often it got stuck (#1858)
	Loops      int `json:"loops,omitempty"`
	Input      int `json:"input"`
	Output     int `json:"output"`
	CacheRead  int `json:"cache_read"`
	CacheWrite int `json:"cache_write"`
	// CacheWrite1h is how many of CacheWrite were 1-hour writes
	CacheWrite1h int     `json:"cache_write_1h,omitempty"`
	Reasoning    int     `json:"reasoning"`
	Cost         float64 `json:"cost"`     // USD at the effective price, for the priced calls
	Unpriced     int     `json:"unpriced"` // calls with tokens but no known price
	// Timed: the answered calls whose first token was timed (streamed),
	// TTFT the sum of their ttft_ms; DecodeMs the time from it to the end
	// of those that wrote any, over which DecodeOut tokens came (a
	// reasoning reply's answer, Record.Decode): their mean wait, and how
	// fast they wrote (#196)
	Timed     int   `json:"timed,omitempty"`
	TTFT      int64 `json:"ttft_ms,omitempty"`
	DecodeMs  int64 `json:"decode_ms,omitempty"`
	DecodeOut int   `json:"decode_out,omitempty"`
	// DecodeN is how many replies DecodeMs and DecodeOut are of: the ones
	// whose speed was measured, which a speed is shown with, so a group of
	// 260 calls whose speed is one reply's says so (Fallmonkey on Discord)
	DecodeN int `json:"decode_n,omitempty"`
	// FloorOut tokens over FloorMs are the least FloorN replies read from a
	// session file wrote at, those whose speed wasn't measured (Row.Floor):
	// what a group shows, as "≥", beside how many it is of, after its
	// measured speed when it has one. They are never in Speed.
	FloorOut int   `json:"floor_out,omitempty"`
	FloorMs  int64 `json:"floor_ms,omitempty"`
	FloorN   int   `json:"floor_n,omitempty"`
}

Totals is a sum of calls.

func (Totals) AllTokens added in v0.1.579

func (t Totals) AllTokens() int

AllTokens is what went in and out and through the cache.

func (Totals) Floor added in v0.1.1180

func (t Totals) Floor() float64

Floor is the least the replies whose speed wasn't measured wrote at, in tokens a second (Row.Floor): 0 for none.

func (Totals) MeanTTFT added in v0.1.392

func (t Totals) MeanTTFT() int64

MeanTTFT is the mean ms to the first token of the timed calls, 0 for none.

func (Totals) Speed added in v0.1.392

func (t Totals) Speed() float64

Speed is how fast the timed calls wrote, in tokens a second after their first: 0 for none.

func (Totals) Tokens

func (t Totals) Tokens() int

Tokens is what went in and out, excluding cache traffic.

type Via added in v0.1.284

type Via struct {
	Provider string    `json:"provider"`
	Model    string    `json:"model"`
	Effort   string    `json:"effort,omitempty"`
	Calls    int       `json:"calls"`
	Tokens   int       `json:"tokens"` // in and out, as Totals counts them
	Last     time.Time `json:"last"`
}

Via is where the gateway sent a session's calls: one model at one reasoning effort, how often and how much.

type Watch added in v0.1.1243

type Watch interface {
	// Attempt is each routing attempt, unbilled failures and retries
	// included, timed as the attempt and not the whole request
	Attempt(Record)
	// End is the request's end: its time, agent, model, status and length
	End(Record)
}

Watch is one request an Observer watches.

Jump to

Keyboard shortcuts

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