feat: token used 展示拆分成 input 和 output

This commit is contained in:
kite 2026-04-29 19:10:45 +08:00
parent 9589cb682e
commit 1c76de02d4
4 changed files with 80 additions and 10 deletions

View file

@ -122,7 +122,7 @@ func runReview(args []string) error {
if len(comments) > 0 {
telemetry.RecordCommentsGenerated(ctx, int64(len(comments)))
}
telemetry.PrintTraceSummary(ag.FilesReviewed(), int64(len(comments)), ag.TotalTokensUsed(), duration)
telemetry.PrintTraceSummary(ag.FilesReviewed(), int64(len(comments)), ag.TotalInputTokens(), ag.TotalOutputTokens(), ag.TotalTokensUsed(), duration)
if opts.outputFormat == "json" {
return outputJSON(comments)

View file

@ -94,7 +94,9 @@ type Agent struct {
totalDeletions int64
currentDate string
session *session.SessionHistory
totalTokensUsed int64 // accumulated tokens from all LLM calls, accessed atomically
totalTokensUsed int64 // accumulated total tokens from all LLM calls, accessed atomically
totalInputTokens int64 // accumulated input/prompt tokens, accessed atomically
totalOutputTokens int64 // accumulated completion tokens, accessed atomically
}
// CommentWorkerPool manages a fixed-size pool of workers dedicated to
@ -225,6 +227,16 @@ func (a *Agent) TotalTokensUsed() int64 {
return atomic.LoadInt64(&a.totalTokensUsed)
}
// TotalInputTokens returns the accumulated input/prompt tokens from all LLM calls.
func (a *Agent) TotalInputTokens() int64 {
return atomic.LoadInt64(&a.totalInputTokens)
}
// TotalOutputTokens returns the accumulated completion tokens from all LLM calls.
func (a *Agent) TotalOutputTokens() int64 {
return atomic.LoadInt64(&a.totalOutputTokens)
}
// loadDiffs populates the diff-related fields.
func (a *Agent) loadDiffs() error {
var provider *diff.Provider
@ -460,6 +472,8 @@ func (a *Agent) executePlanPhase(ctx context.Context, newPath, rawDiff, changeFi
rec.SetResponse(resp, time.Since(startTime))
if resp.Usage != nil {
atomic.AddInt64(&a.totalTokensUsed, int64(resp.Usage.TotalTokens))
atomic.AddInt64(&a.totalInputTokens, int64(resp.Usage.PromptTokens+resp.Usage.CacheReadTokens))
atomic.AddInt64(&a.totalOutputTokens, int64(resp.Usage.CompletionTokens+resp.Usage.CacheWriteTokens))
}
fmt.Printf("[ocr] Plan completed for %s\n", newPath)
return resp.Content(), nil
@ -538,6 +552,8 @@ func (a *Agent) performLlmCodeReview(ctx context.Context, messages []llm.Message
totalTokens := int64(0)
if resp.Usage != nil {
totalTokens = resp.Usage.TotalTokens
atomic.AddInt64(&a.totalInputTokens, int64(resp.Usage.PromptTokens+resp.Usage.CacheReadTokens))
atomic.AddInt64(&a.totalOutputTokens, int64(resp.Usage.CompletionTokens+resp.Usage.CacheWriteTokens))
}
telemetry.RecordLLMRequest(ctx, a.args.Model, duration, totalTokens, "ok")
atomic.AddInt64(&a.totalTokensUsed, totalTokens)
@ -764,6 +780,8 @@ func (a *Agent) compressAndRecord(msgs []llm.Message, filePath string) []llm.Mes
rec.SetResponse(resp, duration)
if resp.Usage != nil {
atomic.AddInt64(&a.totalTokensUsed, int64(resp.Usage.TotalTokens))
atomic.AddInt64(&a.totalInputTokens, int64(resp.Usage.PromptTokens+resp.Usage.CacheReadTokens))
atomic.AddInt64(&a.totalOutputTokens, int64(resp.Usage.CompletionTokens+resp.Usage.CacheWriteTokens))
}
summary := resp.Content()

View file

@ -7,7 +7,35 @@ import (
// UsageInfo holds token usage extracted from an LLM API response.
type UsageInfo struct {
TotalTokens int64 `json:"total_tokens"`
TotalTokens int64 `json:"total_tokens"`
PromptTokens int64 `json:"prompt_tokens"`
CompletionTokens int64 `json:"completion_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens,omitempty"`
CacheWriteTokens int64 `json:"cache_write_tokens,omitempty"`
}
var promptTokensPaths = []string{
"usage.prompt_tokens", // OpenAI standard
"prompt_tokens", // flat at root
"data.usage.prompt_tokens", // wrapped in data layer
}
var completionTokensPaths = []string{
"usage.completion_tokens", // OpenAI standard
"completion_tokens", // flat at root
"data.usage.completion_tokens", // wrapped in data layer
}
var cacheReadTokensPaths = []string{
"usage.cache_read_input_tokens", // Anthropic
"cache_read_input_tokens", // flat at root
"usage.prompt_tokens_details.cache_tokens_hit", // some providers
"usage.prompt_tokens_details.cache_tokens", // some providers
}
var cacheWriteTokensPaths = []string{
"usage.cache_creation_input_tokens", // Anthropic / proxy
"cache_creation_input_tokens", // flat at root
}
// totalTokensPaths is an ordered list of JSON paths to try when extracting
@ -20,18 +48,37 @@ var totalTokensPaths = []string{
}
// resolveUsage parses raw JSON bytes into a map and extracts token usage
// by probing totalTokensPaths sequentially. Returns nil if no path matches.
// by probing configured paths sequentially. Returns nil if no total_tokens found.
func resolveUsage(raw []byte) *UsageInfo {
var rawBody map[string]any
if err := json.Unmarshal(raw, &rawBody); err != nil {
return nil
}
total, ok := probePath(rawBody, totalTokensPaths)
if !ok {
total, hasAny := probePath(rawBody, totalTokensPaths)
prompt, _ := probePath(rawBody, promptTokensPaths)
completion, _ := probePath(rawBody, completionTokensPaths)
cacheRead, _ := probePath(rawBody, cacheReadTokensPaths)
cacheWrite, _ := probePath(rawBody, cacheWriteTokensPaths)
if !hasAny && prompt == 0 && completion == 0 {
return nil
}
return &UsageInfo{TotalTokens: total}
ui := &UsageInfo{
TotalTokens: total,
PromptTokens: prompt,
CompletionTokens: completion,
CacheReadTokens: cacheRead,
CacheWriteTokens: cacheWrite,
}
// If TotalTokens wasn't explicitly available but we have prompt+completion, compute it.
if total == 0 && (prompt > 0 || completion > 0) {
ui.TotalTokens = prompt + completion + cacheRead + cacheWrite
}
return ui
}
// probePath walks through each candidate path in order, returning the first

View file

@ -60,9 +60,14 @@ func FormatDuration(dur time.Duration) string {
}
// PrintTraceSummary prints a one-line summary of the review to stdout.
func PrintTraceSummary(filesReviewed, commentsGenerated int64, totalTokens int64, duration time.Duration) {
fmt.Printf("[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used, %s elapsed\n",
filesReviewed, commentsGenerated, totalTokens, FormatDuration(duration))
func PrintTraceSummary(filesReviewed, commentsGenerated int64, inputTokens, outputTokens, totalTokens int64, duration time.Duration) {
if inputTokens > 0 || outputTokens > 0 {
fmt.Printf("[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used (input: ~%d, output: ~%d), %s elapsed\n",
filesReviewed, commentsGenerated, totalTokens, inputTokens, outputTokens, FormatDuration(duration))
} else {
fmt.Printf("[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used, %s elapsed\n",
filesReviewed, commentsGenerated, totalTokens, FormatDuration(duration))
}
}
// PrintToolCallStarted prints a line when a tool begins execution.