Skip to content

Add cache efficiency, burn rate, and fix context calculation#18

Open
himattm wants to merge 2 commits intomainfrom
himattm/cost-context-accuracy
Open

Add cache efficiency, burn rate, and fix context calculation#18
himattm wants to merge 2 commits intomainfrom
himattm/cost-context-accuracy

Conversation

@himattm
Copy link
Owner

@himattm himattm commented Feb 12, 2026

Summary

  • Fix legacy context calculation: Excluded CacheReadTokens from the context percentage computation — cache reads don't consume additional context window space, so including them inflated the reported usage
  • Extend SessionContext: Added token data fields (InputTokens, OutputTokens, CacheCreationTokens, CacheReadTokens, ContextWindowSize) to make token-level data available to plugins
  • Add cache efficiency indicator (⌁XX%): Shows the percentage of input tokens served from cache, giving visibility into cache hit rates (~10x cheaper tokens)
  • Add session burn rate (~$X.XX/h): Tracks cost velocity using snapshot-based calculation; only appears after 60+ seconds of data and when rate exceeds $0.01/h
  • New burnrate package: Manages snapshot persistence in temp files for cost velocity tracking across status line renders
  • Configurable display: Both indicators can be toggled via show_cache and show_burn_rate in the api_billing config section
  • Cleanup on session end: Burn rate snapshot files are removed when a session ends
  • Updated README: Documented new display format, configuration options, and visual breakdown

Test Plan

  • Verify go test ./... passes (all new and existing tests)
  • Verify legacy context percentage is unchanged when CacheReadTokens is 0
  • Verify legacy context percentage no longer inflates when CacheReadTokens is large
  • Verify cache indicator (⌁XX%) appears when cache read tokens are present
  • Verify cache indicator is hidden when show_cache: false or no cache reads
  • Verify burn rate appears after 60s of session data with cost > $0.01/h
  • Verify burn rate snapshot file is cleaned up on session end
  • Verify config parsing for show_burn_rate and show_cache options

🤖 Generated with Claude Code

…culation

- Fix legacy context calculation to exclude CacheReadTokens (they don't
  consume additional context window space)
- Extend SessionContext with token data fields (input, output, cache
  creation, cache read, context window size)
- Add cache efficiency indicator (⌁XX%) showing cache read hit percentage
- Add session burn rate (~$X.XX/h) with configurable display
- Add burnrate package for snapshot-based cost velocity tracking
- Clean up burn rate snapshot files on session end
- Update README with new API billing display features and config options

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves status/cost reporting by fixing legacy context usage math around cache tokens, exposing token-level fields to plugins, and adding two new API-billing indicators (cache efficiency and session burn rate) with supporting snapshot persistence.

Changes:

  • Fix legacy context % calculation by excluding CacheReadTokens from the denominator.
  • Extend plugin.SessionContext with token usage fields and context window size.
  • Add cache efficiency (⌁XX%) and burn rate (~$X.XX/h) display, plus a new internal/burnrate package and session-end cleanup.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
internal/statusline/statusline.go Fix legacy context math; add burn rate + cache indicator to built-in cost; expose token fields to plugins.
internal/statusline/statusline_test.go Add tests for legacy context cache exclusion and cache indicator/ratio.
internal/statusline/burnrate_test.go Add tests for burn rate rendering and snapshot creation behavior.
internal/plugins/usage.go Add show_cache / show_burn_rate config and cache indicator/burn rate suffix in API billing cost rendering.
internal/plugins/usage_test.go Expand config parsing tests and add cache-indicator + cacheRatio tests.
internal/plugin/types.go Add token-related fields to SessionContext JSON payload for plugins.
internal/hooks/hooks.go Remove burn rate snapshot file on session end.
internal/hooks/hooks_test.go Add test ensuring burn rate snapshot cleanup on session end.
internal/burnrate/burnrate.go New package for snapshot persistence + burn rate calculation/formatting.
internal/burnrate/burnrate_test.go Tests for snapshot lifecycle, rate calculation thresholds, formatting, cleanup.
README.md Document new usage display format and config toggles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 10 to 12
func TestHandleSessionEnd_CleansBurnRateFile(t *testing.T) {
sessionID := "test-session-end-burn"

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses a fixed sessionID (and thus fixed temp file names). This can become flaky when multiple go test runs overlap on the same machine/user (e.g., CI retries) because they share os.TempDir(). Prefer generating a unique session ID per test (timestamp + random suffix) to avoid collisions.

Copilot uses AI. Check for mistakes.
Comment on lines 26 to 27
data, _ := json.Marshal(snap)
os.WriteFile(path, data, 0644)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test setup ignores errors from json.Marshal / os.WriteFile. If either fails, the assertions later can produce misleading results. Please handle these errors and fail fast to make the test output actionable.

Suggested change
data, _ := json.Marshal(snap)
os.WriteFile(path, data, 0644)
data, err := json.Marshal(snap)
if err != nil {
t.Fatalf("failed to marshal burn rate snapshot: %v", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatalf("failed to write burn rate snapshot file: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines 62 to 63
data, _ := json.Marshal(original)
os.WriteFile(path, data, 0644)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test writes the snapshot file without checking the json.Marshal / os.WriteFile errors, which can hide test setup problems and cause confusing failures downstream. Please handle and assert these errors.

Suggested change
data, _ := json.Marshal(original)
os.WriteFile(path, data, 0644)
data, err := json.Marshal(original)
if err != nil {
t.Fatalf("failed to marshal original snapshot: %v", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatalf("failed to write snapshot file: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines 163 to 173
// Append burn rate if enabled and session ID is available
if cfg.showBurnRate && input.Prism.SessionID != "" {
costStr += p.getBurnRateSuffix(input.Prism.SessionID, cost)
}

// Append cache efficiency indicator if enabled
if cfg.showCache {
if ratio, ok := cacheRatio(input.Session.InputTokens, input.Session.CacheCreationTokens, input.Session.CacheReadTokens); ok {
costStr += fmt.Sprintf(" ⌁%d%%", ratio)
}
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Output ordering doesn’t match the README/example format: this appends burn rate before the cache indicator, producing $X.XX ~$Y.YY/h ⌁ZZ% rather than $X.XX ⌁ZZ% ~$Y.YY/h. Either swap the append order in code or adjust the documented format so users get what they expect.

Copilot uses AI. Check for mistakes.
Comment on lines 462 to 478
costStr := fmt.Sprintf("$%.2f", cost)

// Compute burn rate if session ID is available
if sl.input.SessionID != "" {
snap, existed, err := burnrate.LoadOrCreateSnapshot(sl.input.SessionID, cost)
if err == nil && existed {
if rate, show := burnrate.CalculateRate(snap, cost, time.Now()); show {
costStr += " " + burnrate.FormatRate(rate)
}
}
}

// Add cache efficiency indicator
usage := sl.input.Context.CurrentUsage
if ratio, ok := cacheRatio(usage.InputTokens, usage.CacheCreationTokens, usage.CacheReadTokens); ok {
costStr += fmt.Sprintf(" ⌁%d%%", ratio)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README/example format shows cost, then cache indicator, then burn rate, but this builds the string as cost → burn rate → cache. Align the order (or update docs) so the status line output matches the documented display format.

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 58
if err := os.WriteFile(path, data, 0644); err != nil {
return nil, false, err
}

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoadOrCreateSnapshotAt writes the snapshot JSON directly to the target file. With concurrent renders/processes, this can lead to torn writes and JSON unmarshal failures. Consider writing to a temp file and os.Rename for atomic replacement (and/or using a per-session lock) to make snapshot persistence robust.

Suggested change
if err := os.WriteFile(path, data, 0644); err != nil {
return nil, false, err
}
// Write to a temporary file and atomically replace the target to avoid torn writes.
dir := filepath.Dir(path)
tmpFile, err := os.CreateTemp(dir, "prism-burn-*.tmp")
if err != nil {
return nil, false, err
}
tmpName := tmpFile.Name()
if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpName)
return nil, false, err
}
if err := tmpFile.Sync(); err != nil {
tmpFile.Close()
os.Remove(tmpName)
return nil, false, err
}
if err := tmpFile.Close(); err != nil {
os.Remove(tmpName)
return nil, false, err
}
if err := os.Chmod(tmpName, 0644); err != nil {
os.Remove(tmpName)
return nil, false, err
}
if err := os.Rename(tmpName, path); err != nil {
os.Remove(tmpName)
return nil, false, err
}

Copilot uses AI. Check for mistakes.

// Also create idle file (normal cleanup)
idleFile := filepath.Join(os.TempDir(), fmt.Sprintf("prism-idle-%s", sessionID))
os.WriteFile(idleFile, []byte{}, 0644)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test ignores the error from os.WriteFile when creating the idle file, which can mask failures on restricted/readonly temp directories. Please check and fail the test on write errors (similar to the burn-rate file setup above).

Suggested change
os.WriteFile(idleFile, []byte{}, 0644)
if err := os.WriteFile(idleFile, []byte{}, 0644); err != nil {
t.Fatalf("failed to create idle file: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines 465 to 471
if sl.input.SessionID != "" {
snap, existed, err := burnrate.LoadOrCreateSnapshot(sl.input.SessionID, cost)
if err == nil && existed {
if rate, show := burnrate.CalculateRate(snap, cost, time.Now()); show {
costStr += " " + burnrate.FormatRate(rate)
}
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status line sections are rendered concurrently (goroutines in renderLine). If a user config includes both cost and usage sections, burn-rate snapshot file reads/writes can happen at the same time for the same session, risking corrupted/partial JSON reads and flickering output. Consider adding synchronization/atomic file writes in the burnrate package or ensuring only one renderer touches the snapshot per session.

Copilot uses AI. Check for mistakes.
Comment on lines 474 to 478
// Add cache efficiency indicator
usage := sl.input.Context.CurrentUsage
if ratio, ok := cacheRatio(usage.InputTokens, usage.CacheCreationTokens, usage.CacheReadTokens); ok {
costStr += fmt.Sprintf(" ⌁%d%%", ratio)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cache efficiency is appended unconditionally for the built-in cost section, but the PR description/README describe show_cache as a configurable toggle. Either wire show_cache into this code path (similar to the usage plugin), or keep cache efficiency confined to the usage plugin to avoid undocumented behavior differences.

Copilot uses AI. Check for mistakes.
Comment on lines 185 to 187
// Clean up burn rate snapshot file
burnRateFile := filepath.Join(os.TempDir(), fmt.Sprintf("prism-burn-%s", input.SessionID))
os.Remove(burnRateFile)
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates burn-rate path construction logic instead of calling burnrate.Cleanup(sessionID). Using the package helper keeps the cleanup behavior consistent if the burnrate file naming/sanitization changes and reduces the chance of drift.

Copilot uses AI. Check for mistakes.
- Sanitize sessionID in burnrate.FilePath() to prevent path traversal
- Use atomic writes (tmp + rename) in LoadOrCreateSnapshotAt()
- Fix burn rate never appearing: switch to LoadOrCreateSnapshot in usage plugin
- Fix output ordering: cache indicator (⌁XX%) now appears before burn rate (~$X/h)
- Wire show_cache and show_burn_rate config in statusline (skip I/O when disabled)
- Replace manual burn rate path cleanup in hooks with burnrate.Cleanup()
- Add error checks for json.Marshal/os.WriteFile in all test files
- Use unique session IDs per test to prevent collisions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
himattm added a commit that referenced this pull request Feb 13, 2026
- Sanitize idle marker file paths to prevent directory traversal (hooks, statusline)
- Clamp costDecimals config to 0-10 range for safe fmt.Sprintf formatting
- Cache burn rate snapshots in memory via sync.Map to avoid per-render disk I/O
- Fix calculateContextPct to derive used% from RemainingPercentage when UsedPercentage is zero
- Add corrupt/invalid JSON snapshot recovery with Timestamp.IsZero() validation
- Fix unchecked errors in statusline_test.go test setup code
- Extract duplicate cacheRatio logic to shared internal/tokens.CacheEfficiency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant