package stream import ( "context" "strings" "sync" "time" "unicode" "aichat/message" "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model" ) type Stats struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` ToolPromptTokens int `json:"tool_prompt_tokens"` ToolCompletionTokens int `json:"tool_completion_tokens"` TotalTokens int `json:"total_tokens"` CompletionTokensPerSec float64 `json:"completion_tokens_per_sec"` PeakCompletionTokensPerSec float64 `json:"peak_completion_tokens_per_sec"` Estimated bool `json:"estimated"` } type Tracker struct { mu sync.Mutex promptTokens int completionTokens int toolPromptTokens int toolCompletionTokens int } type tokenUsageContextKey struct{} func NewTracker() *Tracker { return &Tracker{} } func ContextWithTracker(ctx context.Context, tracker *Tracker) context.Context { if tracker == nil { return ctx } return context.WithValue(ctx, tokenUsageContextKey{}, tracker) } func TrackerFromContext(ctx context.Context) *Tracker { tracker, _ := ctx.Value(tokenUsageContextKey{}).(*Tracker) return tracker } func (t *Tracker) AddTool(promptTokens, completionTokens int) { if t == nil { return } t.mu.Lock() defer t.mu.Unlock() t.toolPromptTokens += promptTokens t.toolCompletionTokens += completionTokens } func (t *Tracker) SetModel(promptTokens, completionTokens int) { if t == nil { return } t.mu.Lock() defer t.mu.Unlock() t.promptTokens = promptTokens t.completionTokens = completionTokens } func (t *Tracker) Snapshot(tokensPerSecond, peakTokensPerSecond float64) Stats { if t == nil { return Stats{Estimated: true} } t.mu.Lock() defer t.mu.Unlock() total := t.promptTokens + t.completionTokens + t.toolPromptTokens + t.toolCompletionTokens return Stats{ PromptTokens: t.promptTokens, CompletionTokens: t.completionTokens, ToolPromptTokens: t.toolPromptTokens, ToolCompletionTokens: t.toolCompletionTokens, TotalTokens: total, CompletionTokensPerSec: tokensPerSecond, PeakCompletionTokensPerSec: peakTokensPerSecond, Estimated: true, } } func EstimateChatMessagesTokens(messages []message.ChatMessage) int { total := 0 for _, msg := range messages { total += EstimateTokenCount(msg.Role) + EstimateTokenCount(msg.Content) + 4 if msg.ImageURL != "" || msg.ImageURLAlias != "" { total += 85 } } return total } func EstimateArkMessagesTokens(messages []*model.ChatCompletionMessage) int { total := 0 for _, msg := range messages { if msg == nil { continue } total += EstimateTokenCount(string(msg.Role)) + estimateArkContentTokens(msg.Content) + 4 if msg.ToolCallID != "" { total += EstimateTokenCount(msg.ToolCallID) + 2 } for _, call := range msg.ToolCalls { if call == nil { continue } total += EstimateTokenCount(call.ID) + EstimateTokenCount(call.Function.Name) + EstimateTokenCount(call.Function.Arguments) + 8 } } return total } func estimateArkContentTokens(content *model.ChatCompletionMessageContent) int { if content == nil { return 0 } if content.StringValue != nil { return EstimateTokenCount(*content.StringValue) } total := 0 for _, part := range content.ListValue { if part == nil { continue } if part.Text != "" { total += EstimateTokenCount(part.Text) } if part.ImageURL != nil { total += 85 } } return total } func EstimateTokenCount(text string) int { text = strings.TrimSpace(text) if text == "" { return 0 } tokens := 0 asciiRunes := 0 flushASCII := func() { if asciiRunes > 0 { tokens += (asciiRunes + 3) / 4 asciiRunes = 0 } } for _, r := range text { if unicode.IsSpace(r) { flushASCII() continue } if r <= unicode.MaxASCII { asciiRunes++ continue } flushASCII() tokens++ } flushASCII() if tokens == 0 { return 1 } return tokens } func TokensPerSecond(tokens int, start time.Time) float64 { elapsed := time.Since(start).Seconds() if tokens <= 0 || elapsed <= 0 { return 0 } return float64(tokens) / elapsed }