Files
magpie/internal/usage/request_snapshot.go
yetone de5455b8fe gateway, usage, gui: a reply whose vendor sent its first words and held the rest to send in one burst tells no speed, not 1,367 tok/s (John on Discord)
John's request (OpenCode, a group served by Kimi Code at api.kimi.ai,
Chat) showed 1,367 token/s: its tokens counted over the time from its
first token to its end, though nothing came in between and the rest came
at once. The 100 ms / 10,000 tok/s bounds catch a burst only when the
first content comes with it.

firstToken now reads on past the first text and marks when each content
event's bytes came (one mark a millisecond, up to 65,536 marks and the
8 MB it reads), and flowMs is how long the content took to come from
where the speed is counted: the span from a tenth of its bytes to nine
tenths, scaled to the whole. A steady stream gets about its whole window;
one let go in a burst about 1 ms. It is kept as Call/Try/Route .flow and
usage.Record.flow_ms (and the Requests page's packed rows), and
usage.DecodeOf, routing.js's decodeOf and app.js's ledDecode count no
longer a window than it. A record without it reads as before.

Verified: TestBurstTellsNoSpeed, a fake Chat vendor that sends a word,
waits 600 ms, then 199 words and the end in one write (1,000 tokens), for
a Chat and a Responses agent: no speed; before, 1636 and 1655 tok/s. Its
steady case (a word every 60 ms) keeps 200 tokens over most of its
window. TestCodexBackendBurstTellsNoSpeed is the same through Codex's own
ChatGPT backend (before: 1,000 tokens in 601 ms). TestFirstTokenKinds now
expects the stream read past its first text. Go tests ran on Linux
(golang:1.26 in Docker; this Mac's syspolicyd held new test binaries at
launch). GUI: routing-burst-speed (a route with John's 5.1 s first token,
6.7 s and a 2 ms flow: was "1367 tok/s", now none) and usage-speed (a row
with flow_ms 2 shows "—") fail without the JS change and pass with it,
with reasoning-speed, routing-request-metrics and usage-ledger-detail, in
Chromium and WebKit (en/zh, ja/de where the tests have them); no strings
changed. Not reproduced with Kimi Code itself: whether it holds the
stream so is inferred from the numbers.
2026-10-08 20:11:05 +08:00

142 lines
3.4 KiB
Go

package usage
// Cache snapshots encode the fixed request fields without reflection. The
// encoding is private to this process: no on-disk format or migration. Time
// locations are retained separately so a round trip preserves displayed times.
import (
"bytes"
"compress/flate"
"encoding/binary"
"io"
"math"
"sync"
"time"
"unsafe"
)
var rowCompressors = sync.Pool{New: func() any {
w, _ := flate.NewWriter(io.Discard, flate.BestSpeed)
return w
}}
func (c *rowChunk) pack() *rowChunk {
if c == nil || len(c.Rows) < 64 || len(c.Archive) != 0 {
return c
}
data := make([]byte, 0, len(c.Rows)*64)
put := func(v uint64) { data = binary.AppendUvarint(data, v) }
put(uint64(len(c.Rows)))
put(uint64(len(c.Strings)))
for _, s := range c.Strings {
put(uint64(len(s)))
data = append(data, s...)
}
var locations []*time.Location
locs := map[*time.Location]uint64{}
for _, p := range c.Rows {
loc := p.Time.Location()
id, ok := locs[loc]
if !ok {
id = uint64(len(locations))
locs[loc] = id
locations = append(locations, loc)
}
put(uint64(p.Time.Unix()))
put(uint64(p.Time.Nanosecond()))
put(id)
for _, n := range p.Text {
put(uint64(n))
}
for _, n := range p.Tokens {
put(uint64(n))
}
for _, n := range []int64{p.Millis, p.TTFT, p.FirstText, p.Order, p.RouteID, p.Sent, p.Flow} {
put(uint64(n))
}
put(math.Float64bits(p.Cost))
put(uint64(p.Status))
put(uint64(p.Flags))
}
var b bytes.Buffer
w := rowCompressors.Get().(*flate.Writer)
w.Reset(&b)
_, err := w.Write(data)
closeErr := w.Close()
w.Reset(io.Discard)
rowCompressors.Put(w)
if err != nil || closeErr != nil || int64(b.Len()) >= c.Bytes {
return c
}
next := *c
next.Rows, next.Strings, next.dict = nil, nil, nil
next.Archive, next.Locations = b.Bytes(), locations
next.Bytes = int64(len(next.Archive) + len(locations)*8)
return &next
}
func (c *rowChunk) unpack() *rowChunk {
if c == nil || len(c.Archive) == 0 {
return c
}
r := flate.NewReader(bytes.NewReader(c.Archive))
data, err := io.ReadAll(r)
r.Close()
if err != nil {
return nil
}
valid := true
take := func() uint64 {
n, k := binary.Uvarint(data)
if k <= 0 {
valid = false
return 0
}
data = data[k:]
return n
}
rows, strs := take(), take()
if rows > uint64(len(data)) || strs > uint64(len(data)) {
return nil
}
next := *c
next.Archive, next.Locations = nil, nil
next.Rows = make([]packedRow, int(rows))
next.Strings = make([]string, int(strs))
next.Bytes = int64(len(next.Rows)) * int64(unsafe.Sizeof(packedRow{}))
for i := range next.Strings {
n := take()
if n > uint64(len(data)) {
return nil
}
next.Strings[i] = string(data[:n])
data = data[n:]
next.Bytes += int64(n) + 16
}
for i := range next.Rows {
p := &next.Rows[i]
sec, nano, loc := int64(take()), int64(take()), take()
if loc >= uint64(len(c.Locations)) {
return nil
}
p.Time = time.Unix(sec, nano).In(c.Locations[loc])
for j := range p.Text {
n := take()
if n >= strs {
return nil
}
p.Text[j] = uint32(n)
}
for j := range p.Tokens {
p.Tokens[j] = int64(take())
}
p.Millis, p.TTFT, p.FirstText, p.Order = int64(take()), int64(take()), int64(take()), int64(take())
p.RouteID = int64(take())
p.Sent, p.Flow = int64(take()), int64(take())
p.Cost, p.Status, p.Flags = math.Float64frombits(take()), int32(take()), uint8(take())
}
if !valid || len(data) != 0 {
return nil
}
return &next
}