From 5f3fc4c133b289dfa1e203d73ada27c0a8e49a3a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Feb 2026 06:03:19 +0000 Subject: [PATCH] feat: add /usage command to show model info and token usage Adds token usage tracking to AgentInstance using atomic counters (TotalPromptTokens, TotalCompletionTokens, TotalRequests) that accumulate after each LLM call. The /usage command displays current model name, max tokens, temperature, and session token statistics. Available in all interactive modes like /help. https://claude.ai/code/session_01PoHBfe7eKmhHvVF12W3xZ2 --- cmd/picoclaw/cmd_agent.go | 43 +++++++++++++++++++++++++++++++++++++-- pkg/agent/instance.go | 16 +++++++++++++++ pkg/agent/loop.go | 22 ++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/cmd/picoclaw/cmd_agent.go b/cmd/picoclaw/cmd_agent.go index 854d23709..58a63cc99 100644 --- a/cmd/picoclaw/cmd_agent.go +++ b/cmd/picoclaw/cmd_agent.go @@ -109,6 +109,7 @@ func agentCmd() { } else { fmt.Printf("%s Interactive mode (Ctrl+C to exit)\n", logo) fmt.Println(" /help - show detailed help") + fmt.Println(" /usage - show model info and token usage") fmt.Println(" /cmd - switch to command mode") fmt.Println(" /pico - switch to chat mode") fmt.Println(" /hipico - AI assistance in command mode") @@ -163,11 +164,15 @@ func interactiveMode(agentLoop *agent.AgentLoop, sessionKey string) { return } - // /help works in all modes + // /help and /usage work in all modes if input == "/help" { printHelp() continue } + if input == "/usage" { + printUsage(agentLoop) + continue + } switch mode { case modePico: @@ -284,11 +289,15 @@ func simpleInteractiveMode(agentLoop *agent.AgentLoop, sessionKey string) { return } - // /help works in all modes + // /help and /usage work in all modes if input == "/help" { printHelp() continue } + if input == "/usage" { + printUsage(agentLoop) + continue + } switch mode { case modePico: @@ -383,6 +392,7 @@ PicoClaw has three interactive modes: Commands (available in all modes): /help Show this help message + /usage Show model info and token usage exit Exit PicoClaw quit Exit PicoClaw Ctrl+C Exit PicoClaw @@ -410,6 +420,35 @@ Examples: `, logo, logo, logo, logo) } +// printUsage displays current model information and accumulated token usage. +func printUsage(agentLoop *agent.AgentLoop) { + info := agentLoop.GetUsageInfo() + if info == nil { + fmt.Println("No usage information available.") + return + } + fmt.Printf(`%s Usage +━━━━━━━━━━━━━━━━━━━━━━ +Model: %s +Max tokens: %d +Temperature: %.1f + +Token usage (this session): + Prompt tokens: %d + Completion tokens:%d + Total tokens: %d + Requests: %d +`, logo, + info["model"], + info["max_tokens"], + info["temperature"], + info["prompt_tokens"], + info["completion_tokens"], + info["total_tokens"], + info["requests"], + ) +} + // executeShellCommand runs a shell command in the current working directory // and prints the output. It also handles the cd command to change directories. func executeShellCommand(input string) { diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index dfbef9fbc..4ce7ad23a 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -4,6 +4,7 @@ import ( "os" "path/filepath" "strings" + "sync/atomic" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/providers" @@ -31,6 +32,21 @@ type AgentInstance struct { Subagents *config.SubagentsConfig SkillsFilter []string Candidates []providers.FallbackCandidate + + // Accumulated token usage counters (atomic for concurrent safety) + TotalPromptTokens atomic.Int64 + TotalCompletionTokens atomic.Int64 + TotalRequests atomic.Int64 +} + +// AddUsage accumulates token usage from a single LLM response. +func (a *AgentInstance) AddUsage(usage *providers.UsageInfo) { + if usage == nil { + return + } + a.TotalPromptTokens.Add(int64(usage.PromptTokens)) + a.TotalCompletionTokens.Add(int64(usage.CompletionTokens)) + a.TotalRequests.Add(1) } // NewAgentInstance creates an agent instance from config. diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index bf229ad74..edcc04602 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -594,6 +594,9 @@ func (al *AgentLoop) runLLMIteration( return "", iteration, fmt.Errorf("LLM call failed after retries: %w", err) } + // Accumulate token usage + agent.AddUsage(response.Usage) + // Check if no tool calls - we're done if len(response.ToolCalls) == 0 { finalContent = response.Content @@ -852,6 +855,25 @@ func (al *AgentLoop) GetStartupInfo() map[string]any { return info } +// GetUsageInfo returns accumulated token usage for the default agent. +func (al *AgentLoop) GetUsageInfo() map[string]any { + agent := al.registry.GetDefaultAgent() + if agent == nil { + return nil + } + promptTokens := agent.TotalPromptTokens.Load() + completionTokens := agent.TotalCompletionTokens.Load() + return map[string]any{ + "model": agent.Model, + "max_tokens": agent.MaxTokens, + "temperature": agent.Temperature, + "prompt_tokens": promptTokens, + "completion_tokens": completionTokens, + "total_tokens": promptTokens + completionTokens, + "requests": agent.TotalRequests.Load(), + } +} + // formatMessagesForLog formats messages for logging func formatMessagesForLog(messages []providers.Message) string { if len(messages) == 0 {