added correct reasoning token stats

This commit is contained in:
Luxferre
2026-09-11 10:55:19 +03:00
parent d9c7bb4407
commit f21dc0efde
2 changed files with 112 additions and 8 deletions
+38 -8
View File
@@ -857,7 +857,13 @@ type Usage struct {
CachedTokens int `json:"cached_tokens"`
} `json:"prompt_tokens_details"`
CachedTokens int `json:"cached_tokens"`
Model string `json:"-"`
// ContextTokens is a local estimate of the total tokens occupying the context
// window for this sample, including any reasoning_content that is resent as
// part of the context. It acts as a floor beneath PromptTokens so reasoning
// tokens are always counted in the context window statistics even when the
// provider under-reports prompt_tokens.
ContextTokens int `json:"-"`
Model string `json:"-"`
}
func (u Usage) Cached() int {
@@ -882,26 +888,39 @@ func estTokens(msgs []Message) int {
return t
}
// ctxTokens returns the number of tokens occupying the context window for a
// usage sample. We prefer the provider's reported prompt token count, but never
// go below a local estimate of the messages we actually sent (which includes any
// reasoning_content resent as context) so reasoning tokens are always counted in
// the context window statistics.
func ctxTokens(u Usage) int {
if u.ContextTokens > u.PromptTokens {
return u.ContextTokens
}
return u.PromptTokens
}
func contextPct(u Usage, cw int) float64 {
if cw <= 0 { cw = 262144 }
return float64(u.PromptTokens) * 100.0 / float64(cw)
return float64(ctxTokens(u)) * 100.0 / float64(cw)
}
func formatUsage(u Usage, cw int) string {
pct := contextPct(u, cw)
base := ctxTokens(u)
pct := float64(base) * 100.0 / float64(cw)
cached := u.Cached()
if cached > 0 {
uncached := u.PromptTokens - cached
if uncached < 0 { uncached = 0 }
if u.Model != "" {
return fmt.Sprintf("[%s: %d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.Model, u.PromptTokens, cached, uncached, u.CompletionTokens, u.PromptTokens, cw, pct)
return fmt.Sprintf("[%s: %d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.Model, u.PromptTokens, cached, uncached, u.CompletionTokens, base, cw, pct)
}
return fmt.Sprintf("[%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, cached, uncached, u.CompletionTokens, u.PromptTokens, cw, pct)
return fmt.Sprintf("[%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, cached, uncached, u.CompletionTokens, base, cw, pct)
}
if u.Model != "" {
return fmt.Sprintf("[%s: %d prompt + %d completion | context: %d/%d (%.1f%%)]", u.Model, u.PromptTokens, u.CompletionTokens, u.PromptTokens, cw, pct)
return fmt.Sprintf("[%s: %d prompt + %d completion | context: %d/%d (%.1f%%)]", u.Model, u.PromptTokens, u.CompletionTokens, base, cw, pct)
}
return fmt.Sprintf("[%d prompt + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, u.CompletionTokens, u.PromptTokens, cw, pct)
return fmt.Sprintf("[%d prompt + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, u.CompletionTokens, base, cw, pct)
}
type streamDelta struct {
@@ -1078,6 +1097,9 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
if m.ReasoningContent == "" { m.ReasoningContent = cr.Choices[0].Message.Thought }
u := cr.Usage
if cr.Model != "" { u.Model = cr.Model }
// Local estimate of the full context (including any resent reasoning) so it
// is counted in the context window statistics as a floor under prompt_tokens.
u.ContextTokens = estTokens(msgs)
if u.PromptTokens == 0 {
u.PromptTokens = estTokens(msgs)
u.CompletionTokens = estTokens([]Message{m})
@@ -1091,6 +1113,11 @@ func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any)
u.CompletionTokens = estTokens([]Message{m})
u.TotalTokens = u.PromptTokens + u.CompletionTokens
}
if err == nil {
// Local estimate of the full context (including any resent reasoning) so it
// is counted in the context window statistics as a floor under prompt_tokens.
u.ContextTokens = estTokens(msgs)
}
return m, u, err
}
@@ -1327,6 +1354,9 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
turnUsage.PromptTokens = u.PromptTokens
turnUsage.CompletionTokens += u.CompletionTokens
turnUsage.TotalTokens += u.TotalTokens
// The last call sends the full conversation, so its context estimate is the
// authoritative one for the turn's context window statistics.
turnUsage.ContextTokens = u.ContextTokens
if u.Cached() > 0 { turnUsage.CachedTokens = u.Cached() }
if u.Model != "" { turnUsage.Model = u.Model }
for j := range m.ToolCalls {
@@ -2217,7 +2247,7 @@ func main() {
fmt.Println(c(formatUsage(usg, cfg.ContextWindow), 2))
pct := contextPct(usg, cfg.ContextWindow)
if pct >= 60.0 && len(msgs) > 1 {
fmt.Print(c(fmt.Sprintf("Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: ", pct, usg.PromptTokens, cfg.ContextWindow), 33))
fmt.Print(c(fmt.Sprintf("Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: ", pct, ctxTokens(usg), cfg.ContextWindow), 33))
if ans, ok := readPlain(""); ok {
ans = strings.TrimSpace(strings.ToLower(ans))
if ans == "" || ans == "y" || ans == "yes" {