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.
142 lines
3.4 KiB
Go
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
|
|
}
|