mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
The session's first executor request pays for the prefix every later one reuses, so the session-wide hit rate mixes "did the cache work once warm" with "how much a cold start could reuse". usagereport already records the cold start, but nothing a live session reads projected it. The live figures come from the serve ledger, which restarts on /new, /resume and /fork; usageTally does not, so its ColdStart can belong to an earlier conversation in the same process. The broadcaster therefore takes the cold start from the ledger's own population: the first request the ledger files under the executor (pricing.LedgerSource, the rule Add already used), split by the same CacheSplit the ledger applies, and reset with it. SessionUsage carries it as coldStart. - /status (TUI): "warm cache" row, ledger hit rate with the cold start taken out of hit, miss and unknown; "out/in" row, output ÷ input. - Desktop session overview behind the cache meter: the same two figures, computed in state/overview beside cacheRate. - wire-parity: ColdStartUsage mirrored in port/session.ts.
102 lines
3.9 KiB
Go
102 lines
3.9 KiB
Go
package serve
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"reasonix/internal/contract/pricing"
|
|
)
|
|
|
|
// SourceUsage is what one usage source (the main agent, a sub-agent, compaction)
|
|
// has spent since this session was opened in this run. Requests counts provider
|
|
// responses that reported usage with a cost quote; a request that reported none
|
|
// is in no figure here. CacheUnknown is prompt the endpoint did not split into
|
|
// cached and uncached, which is billed as uncached and left out of a hit rate.
|
|
type SourceUsage struct {
|
|
Source string `json:"source"`
|
|
Requests int `json:"requests"`
|
|
PromptTokens int `json:"promptTokens"`
|
|
CompletionTokens int `json:"completionTokens"`
|
|
CacheHit int `json:"cacheHit"`
|
|
CacheMiss int `json:"cacheMiss"`
|
|
CacheUnknown int `json:"cacheUnknown,omitempty"`
|
|
Cost pricing.CostQuote `json:"cost"`
|
|
}
|
|
|
|
// SessionUsage is the session's usage grouped by source, read from the same
|
|
// ledger the cost total is. It begins when the ledger does: a new, resumed or
|
|
// forked session starts it again, so these are totals since this session was
|
|
// opened in this run, not over the conversation's whole life.
|
|
type SessionUsage struct {
|
|
Requests int `json:"requests"`
|
|
PromptTokens int `json:"promptTokens"`
|
|
CompletionTokens int `json:"completionTokens"`
|
|
CacheHit int `json:"cacheHit"`
|
|
CacheMiss int `json:"cacheMiss"`
|
|
CacheUnknown int `json:"cacheUnknown,omitempty"`
|
|
WorkMs int64 `json:"workMs"`
|
|
Sources []SourceUsage `json:"sources"`
|
|
// ColdStart is the first executor request in these totals, which it is also
|
|
// in; subtracting it gives the rate once the prefix it paid for was warm.
|
|
ColdStart *ColdStartUsage `json:"coldStart,omitempty"`
|
|
}
|
|
|
|
// ColdStartUsage is the session's opening executor request, split the way the
|
|
// ledger split it. It pays for the prefix every later request reuses, so it is
|
|
// the one request a warm hit rate leaves out.
|
|
type ColdStartUsage struct {
|
|
PromptTokens int `json:"promptTokens"`
|
|
CacheHit int `json:"cacheHit"`
|
|
CacheMiss int `json:"cacheMiss"`
|
|
CacheUnknown int `json:"cacheUnknown,omitempty"`
|
|
}
|
|
|
|
func coldStartOf(tokens pricing.UsageTokens) *ColdStartUsage {
|
|
hit, miss, unknown := tokens.CacheSplit()
|
|
return &ColdStartUsage{PromptTokens: tokens.PromptTokens, CacheHit: hit, CacheMiss: miss, CacheUnknown: unknown}
|
|
}
|
|
|
|
// SessionUsage totals the ledger by source. WorkMs is the answered work time of
|
|
// the turns that gave a final answer; failed and cancelled turns add nothing.
|
|
func (b *Broadcaster) SessionUsage() SessionUsage {
|
|
out := SessionUsage{Sources: []SourceUsage{}}
|
|
if b == nil {
|
|
return out
|
|
}
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
out.WorkMs = b.workMs
|
|
if b.coldStart != nil {
|
|
cold := *b.coldStart
|
|
out.ColdStart = &cold
|
|
}
|
|
grouped := b.ledger.EntriesBySource()
|
|
names := make([]string, 0, len(grouped))
|
|
for name := range grouped {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
for _, name := range names {
|
|
src := SourceUsage{Source: name}
|
|
quotes := make([]pricing.CostQuote, 0, len(grouped[name]))
|
|
for _, ent := range grouped[name] {
|
|
src.Requests += ent.RequestCount
|
|
src.PromptTokens += ent.PromptTokens
|
|
src.CompletionTokens += ent.CompletionTokens
|
|
src.CacheHit += ent.CacheHitTokens
|
|
src.CacheMiss += ent.CacheMissTokens
|
|
src.CacheUnknown += ent.CacheUnknownTokens
|
|
quotes = append(quotes, ent.Quote)
|
|
}
|
|
sort.Slice(quotes, func(i, j int) bool { return quotes[i].ModelRef < quotes[j].ModelRef })
|
|
src.Cost = pricing.AggregateQuotes(quotes, b.displayCurrency)
|
|
out.Requests += src.Requests
|
|
out.PromptTokens += src.PromptTokens
|
|
out.CompletionTokens += src.CompletionTokens
|
|
out.CacheHit += src.CacheHit
|
|
out.CacheMiss += src.CacheMiss
|
|
out.CacheUnknown += src.CacheUnknown
|
|
out.Sources = append(out.Sources, src)
|
|
}
|
|
return out
|
|
}
|