@hasna/terminal 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/App.js +404 -0
- package/dist/Browse.js +79 -0
- package/dist/FuzzyPicker.js +47 -0
- package/dist/Onboarding.js +51 -0
- package/dist/Spinner.js +12 -0
- package/dist/StatusBar.js +49 -0
- package/dist/ai.js +322 -0
- package/dist/cache.js +41 -0
- package/dist/cli.js +64 -16
- package/dist/command-rewriter.js +64 -0
- package/dist/command-validator.js +86 -0
- package/dist/compression.js +107 -0
- package/dist/context-hints.js +275 -0
- package/dist/diff-cache.js +107 -0
- package/dist/discover.js +212 -0
- package/dist/economy.js +123 -0
- package/dist/expand-store.js +38 -0
- package/dist/file-cache.js +72 -0
- package/dist/file-index.js +62 -0
- package/dist/history.js +62 -0
- package/dist/lazy-executor.js +54 -0
- package/dist/line-dedup.js +59 -0
- package/dist/loop-detector.js +75 -0
- package/dist/mcp/install.js +98 -0
- package/dist/mcp/server.js +569 -0
- package/dist/noise-filter.js +86 -0
- package/dist/output-processor.js +129 -0
- package/dist/output-router.js +41 -0
- package/dist/output-store.js +111 -0
- package/dist/parsers/base.js +2 -0
- package/dist/parsers/build.js +64 -0
- package/dist/parsers/errors.js +101 -0
- package/dist/parsers/files.js +78 -0
- package/dist/parsers/git.js +99 -0
- package/dist/parsers/index.js +48 -0
- package/dist/parsers/tests.js +89 -0
- package/dist/providers/anthropic.js +39 -0
- package/dist/providers/base.js +4 -0
- package/dist/providers/cerebras.js +95 -0
- package/dist/providers/groq.js +95 -0
- package/dist/providers/index.js +73 -0
- package/dist/providers/xai.js +95 -0
- package/dist/recipes/model.js +20 -0
- package/dist/recipes/storage.js +136 -0
- package/dist/search/content-search.js +68 -0
- package/dist/search/file-search.js +61 -0
- package/dist/search/filters.js +34 -0
- package/dist/search/index.js +5 -0
- package/dist/search/semantic.js +320 -0
- package/dist/session-boot.js +59 -0
- package/dist/session-context.js +55 -0
- package/dist/sessions-db.js +173 -0
- package/dist/smart-display.js +286 -0
- package/dist/snapshots.js +51 -0
- package/dist/supervisor.js +112 -0
- package/dist/test-watchlist.js +131 -0
- package/dist/tool-profiles.js +122 -0
- package/dist/tree.js +94 -0
- package/dist/usage-cache.js +65 -0
- package/package.json +8 -1
- package/src/ai.ts +8 -0
- package/src/cli.tsx +57 -18
- package/src/output-processor.ts +6 -1
- package/src/output-store.ts +58 -12
- package/src/tool-profiles.ts +139 -0
- package/.claude/scheduled_tasks.lock +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -14
- package/CONTRIBUTING.md +0 -80
- package/benchmarks/benchmark.mjs +0 -115
- package/imported_modules.txt +0 -0
- package/temp/rtk/.claude/agents/code-reviewer.md +0 -221
- package/temp/rtk/.claude/agents/debugger.md +0 -519
- package/temp/rtk/.claude/agents/rtk-testing-specialist.md +0 -461
- package/temp/rtk/.claude/agents/rust-rtk.md +0 -511
- package/temp/rtk/.claude/agents/technical-writer.md +0 -355
- package/temp/rtk/.claude/commands/diagnose.md +0 -352
- package/temp/rtk/.claude/commands/test-routing.md +0 -362
- package/temp/rtk/.claude/hooks/bash/pre-commit-format.sh +0 -16
- package/temp/rtk/.claude/hooks/rtk-rewrite.sh +0 -70
- package/temp/rtk/.claude/hooks/rtk-suggest.sh +0 -152
- package/temp/rtk/.claude/rules/cli-testing.md +0 -526
- package/temp/rtk/.claude/skills/issue-triage/SKILL.md +0 -348
- package/temp/rtk/.claude/skills/issue-triage/templates/issue-comment.md +0 -134
- package/temp/rtk/.claude/skills/performance.md +0 -435
- package/temp/rtk/.claude/skills/pr-triage/SKILL.md +0 -315
- package/temp/rtk/.claude/skills/pr-triage/templates/review-comment.md +0 -71
- package/temp/rtk/.claude/skills/repo-recap.md +0 -206
- package/temp/rtk/.claude/skills/rtk-tdd/SKILL.md +0 -78
- package/temp/rtk/.claude/skills/rtk-tdd/references/testing-patterns.md +0 -124
- package/temp/rtk/.claude/skills/security-guardian.md +0 -503
- package/temp/rtk/.claude/skills/ship.md +0 -404
- package/temp/rtk/.github/workflows/benchmark.yml +0 -34
- package/temp/rtk/.github/workflows/dco-check.yaml +0 -12
- package/temp/rtk/.github/workflows/release-please.yml +0 -51
- package/temp/rtk/.github/workflows/release.yml +0 -343
- package/temp/rtk/.github/workflows/security-check.yml +0 -135
- package/temp/rtk/.github/workflows/validate-docs.yml +0 -78
- package/temp/rtk/.release-please-manifest.json +0 -3
- package/temp/rtk/ARCHITECTURE.md +0 -1491
- package/temp/rtk/CHANGELOG.md +0 -640
- package/temp/rtk/CLAUDE.md +0 -605
- package/temp/rtk/CONTRIBUTING.md +0 -199
- package/temp/rtk/Cargo.lock +0 -1668
- package/temp/rtk/Cargo.toml +0 -64
- package/temp/rtk/Formula/rtk.rb +0 -43
- package/temp/rtk/INSTALL.md +0 -390
- package/temp/rtk/LICENSE +0 -21
- package/temp/rtk/README.md +0 -386
- package/temp/rtk/README_es.md +0 -159
- package/temp/rtk/README_fr.md +0 -197
- package/temp/rtk/README_ja.md +0 -159
- package/temp/rtk/README_ko.md +0 -159
- package/temp/rtk/README_zh.md +0 -167
- package/temp/rtk/ROADMAP.md +0 -15
- package/temp/rtk/SECURITY.md +0 -217
- package/temp/rtk/TEST_EXEC_TIME.md +0 -102
- package/temp/rtk/build.rs +0 -57
- package/temp/rtk/docs/AUDIT_GUIDE.md +0 -432
- package/temp/rtk/docs/FEATURES.md +0 -1410
- package/temp/rtk/docs/TROUBLESHOOTING.md +0 -309
- package/temp/rtk/docs/filter-workflow.md +0 -102
- package/temp/rtk/docs/images/gain-dashboard.jpg +0 -0
- package/temp/rtk/docs/tracking.md +0 -583
- package/temp/rtk/hooks/opencode-rtk.ts +0 -39
- package/temp/rtk/hooks/rtk-awareness.md +0 -29
- package/temp/rtk/hooks/rtk-rewrite.sh +0 -61
- package/temp/rtk/hooks/test-rtk-rewrite.sh +0 -442
- package/temp/rtk/install.sh +0 -124
- package/temp/rtk/release-please-config.json +0 -10
- package/temp/rtk/scripts/benchmark.sh +0 -592
- package/temp/rtk/scripts/check-installation.sh +0 -162
- package/temp/rtk/scripts/install-local.sh +0 -37
- package/temp/rtk/scripts/rtk-economics.sh +0 -137
- package/temp/rtk/scripts/test-all.sh +0 -561
- package/temp/rtk/scripts/test-aristote.sh +0 -227
- package/temp/rtk/scripts/test-tracking.sh +0 -79
- package/temp/rtk/scripts/update-readme-metrics.sh +0 -32
- package/temp/rtk/scripts/validate-docs.sh +0 -73
- package/temp/rtk/src/aws_cmd.rs +0 -880
- package/temp/rtk/src/binlog.rs +0 -1645
- package/temp/rtk/src/cargo_cmd.rs +0 -1727
- package/temp/rtk/src/cc_economics.rs +0 -1157
- package/temp/rtk/src/ccusage.rs +0 -340
- package/temp/rtk/src/config.rs +0 -187
- package/temp/rtk/src/container.rs +0 -855
- package/temp/rtk/src/curl_cmd.rs +0 -134
- package/temp/rtk/src/deps.rs +0 -268
- package/temp/rtk/src/diff_cmd.rs +0 -367
- package/temp/rtk/src/discover/mod.rs +0 -274
- package/temp/rtk/src/discover/provider.rs +0 -388
- package/temp/rtk/src/discover/registry.rs +0 -2022
- package/temp/rtk/src/discover/report.rs +0 -202
- package/temp/rtk/src/discover/rules.rs +0 -667
- package/temp/rtk/src/display_helpers.rs +0 -402
- package/temp/rtk/src/dotnet_cmd.rs +0 -1771
- package/temp/rtk/src/dotnet_format_report.rs +0 -133
- package/temp/rtk/src/dotnet_trx.rs +0 -593
- package/temp/rtk/src/env_cmd.rs +0 -204
- package/temp/rtk/src/filter.rs +0 -462
- package/temp/rtk/src/filters/README.md +0 -52
- package/temp/rtk/src/filters/ansible-playbook.toml +0 -34
- package/temp/rtk/src/filters/basedpyright.toml +0 -47
- package/temp/rtk/src/filters/biome.toml +0 -45
- package/temp/rtk/src/filters/brew-install.toml +0 -37
- package/temp/rtk/src/filters/composer-install.toml +0 -40
- package/temp/rtk/src/filters/df.toml +0 -16
- package/temp/rtk/src/filters/dotnet-build.toml +0 -64
- package/temp/rtk/src/filters/du.toml +0 -16
- package/temp/rtk/src/filters/fail2ban-client.toml +0 -15
- package/temp/rtk/src/filters/gcc.toml +0 -49
- package/temp/rtk/src/filters/gcloud.toml +0 -22
- package/temp/rtk/src/filters/hadolint.toml +0 -24
- package/temp/rtk/src/filters/helm.toml +0 -29
- package/temp/rtk/src/filters/iptables.toml +0 -27
- package/temp/rtk/src/filters/jj.toml +0 -28
- package/temp/rtk/src/filters/jq.toml +0 -24
- package/temp/rtk/src/filters/make.toml +0 -41
- package/temp/rtk/src/filters/markdownlint.toml +0 -24
- package/temp/rtk/src/filters/mix-compile.toml +0 -27
- package/temp/rtk/src/filters/mix-format.toml +0 -15
- package/temp/rtk/src/filters/mvn-build.toml +0 -44
- package/temp/rtk/src/filters/oxlint.toml +0 -43
- package/temp/rtk/src/filters/ping.toml +0 -63
- package/temp/rtk/src/filters/pio-run.toml +0 -40
- package/temp/rtk/src/filters/poetry-install.toml +0 -50
- package/temp/rtk/src/filters/pre-commit.toml +0 -35
- package/temp/rtk/src/filters/ps.toml +0 -16
- package/temp/rtk/src/filters/quarto-render.toml +0 -41
- package/temp/rtk/src/filters/rsync.toml +0 -48
- package/temp/rtk/src/filters/shellcheck.toml +0 -27
- package/temp/rtk/src/filters/shopify-theme.toml +0 -29
- package/temp/rtk/src/filters/skopeo.toml +0 -45
- package/temp/rtk/src/filters/sops.toml +0 -16
- package/temp/rtk/src/filters/ssh.toml +0 -44
- package/temp/rtk/src/filters/stat.toml +0 -34
- package/temp/rtk/src/filters/swift-build.toml +0 -41
- package/temp/rtk/src/filters/systemctl-status.toml +0 -33
- package/temp/rtk/src/filters/terraform-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-fmt.toml +0 -16
- package/temp/rtk/src/filters/tofu-init.toml +0 -38
- package/temp/rtk/src/filters/tofu-plan.toml +0 -35
- package/temp/rtk/src/filters/tofu-validate.toml +0 -17
- package/temp/rtk/src/filters/trunk-build.toml +0 -39
- package/temp/rtk/src/filters/ty.toml +0 -50
- package/temp/rtk/src/filters/uv-sync.toml +0 -37
- package/temp/rtk/src/filters/xcodebuild.toml +0 -99
- package/temp/rtk/src/filters/yamllint.toml +0 -25
- package/temp/rtk/src/find_cmd.rs +0 -598
- package/temp/rtk/src/format_cmd.rs +0 -386
- package/temp/rtk/src/gain.rs +0 -723
- package/temp/rtk/src/gh_cmd.rs +0 -1651
- package/temp/rtk/src/git.rs +0 -2012
- package/temp/rtk/src/go_cmd.rs +0 -592
- package/temp/rtk/src/golangci_cmd.rs +0 -254
- package/temp/rtk/src/grep_cmd.rs +0 -288
- package/temp/rtk/src/gt_cmd.rs +0 -810
- package/temp/rtk/src/hook_audit_cmd.rs +0 -283
- package/temp/rtk/src/hook_check.rs +0 -171
- package/temp/rtk/src/init.rs +0 -1859
- package/temp/rtk/src/integrity.rs +0 -537
- package/temp/rtk/src/json_cmd.rs +0 -231
- package/temp/rtk/src/learn/detector.rs +0 -628
- package/temp/rtk/src/learn/mod.rs +0 -119
- package/temp/rtk/src/learn/report.rs +0 -184
- package/temp/rtk/src/lint_cmd.rs +0 -694
- package/temp/rtk/src/local_llm.rs +0 -316
- package/temp/rtk/src/log_cmd.rs +0 -248
- package/temp/rtk/src/ls.rs +0 -324
- package/temp/rtk/src/main.rs +0 -2482
- package/temp/rtk/src/mypy_cmd.rs +0 -389
- package/temp/rtk/src/next_cmd.rs +0 -241
- package/temp/rtk/src/npm_cmd.rs +0 -236
- package/temp/rtk/src/parser/README.md +0 -267
- package/temp/rtk/src/parser/error.rs +0 -46
- package/temp/rtk/src/parser/formatter.rs +0 -336
- package/temp/rtk/src/parser/mod.rs +0 -311
- package/temp/rtk/src/parser/types.rs +0 -119
- package/temp/rtk/src/pip_cmd.rs +0 -302
- package/temp/rtk/src/playwright_cmd.rs +0 -479
- package/temp/rtk/src/pnpm_cmd.rs +0 -573
- package/temp/rtk/src/prettier_cmd.rs +0 -221
- package/temp/rtk/src/prisma_cmd.rs +0 -482
- package/temp/rtk/src/psql_cmd.rs +0 -382
- package/temp/rtk/src/pytest_cmd.rs +0 -384
- package/temp/rtk/src/read.rs +0 -217
- package/temp/rtk/src/rewrite_cmd.rs +0 -50
- package/temp/rtk/src/ruff_cmd.rs +0 -402
- package/temp/rtk/src/runner.rs +0 -271
- package/temp/rtk/src/summary.rs +0 -297
- package/temp/rtk/src/tee.rs +0 -405
- package/temp/rtk/src/telemetry.rs +0 -248
- package/temp/rtk/src/toml_filter.rs +0 -1655
- package/temp/rtk/src/tracking.rs +0 -1416
- package/temp/rtk/src/tree.rs +0 -209
- package/temp/rtk/src/tsc_cmd.rs +0 -259
- package/temp/rtk/src/utils.rs +0 -432
- package/temp/rtk/src/verify_cmd.rs +0 -47
- package/temp/rtk/src/vitest_cmd.rs +0 -385
- package/temp/rtk/src/wc_cmd.rs +0 -401
- package/temp/rtk/src/wget_cmd.rs +0 -260
- package/temp/rtk/tests/fixtures/dotnet/build_failed.txt +0 -11
- package/temp/rtk/tests/fixtures/dotnet/format_changes.json +0 -31
- package/temp/rtk/tests/fixtures/dotnet/format_empty.json +0 -1
- package/temp/rtk/tests/fixtures/dotnet/format_success.json +0 -12
- package/temp/rtk/tests/fixtures/dotnet/test_failed.txt +0 -18
- package/tsconfig.json +0 -15
|
@@ -1,1157 +0,0 @@
|
|
|
1
|
-
//! Claude Code Economics: Spending vs Savings Analysis
|
|
2
|
-
//!
|
|
3
|
-
//! Combines ccusage (tokens spent) with rtk tracking (tokens saved) to provide
|
|
4
|
-
//! dual-metric economic impact reporting with blended and active cost-per-token.
|
|
5
|
-
|
|
6
|
-
use anyhow::{Context, Result};
|
|
7
|
-
use chrono::NaiveDate;
|
|
8
|
-
use serde::Serialize;
|
|
9
|
-
use std::collections::HashMap;
|
|
10
|
-
|
|
11
|
-
use crate::ccusage::{self, CcusagePeriod, Granularity};
|
|
12
|
-
use crate::tracking::{DayStats, MonthStats, Tracker, WeekStats};
|
|
13
|
-
use crate::utils::{format_cpt, format_tokens, format_usd};
|
|
14
|
-
|
|
15
|
-
// ── Constants ──
|
|
16
|
-
|
|
17
|
-
const BILLION: f64 = 1e9;
|
|
18
|
-
|
|
19
|
-
// API pricing ratios (verified Feb 2026, consistent across Claude models <=200K context)
|
|
20
|
-
// Source: https://docs.anthropic.com/en/docs/about-claude/models
|
|
21
|
-
const WEIGHT_OUTPUT: f64 = 5.0; // Output = 5x input
|
|
22
|
-
const WEIGHT_CACHE_CREATE: f64 = 1.25; // Cache write = 1.25x input
|
|
23
|
-
const WEIGHT_CACHE_READ: f64 = 0.1; // Cache read = 0.1x input
|
|
24
|
-
|
|
25
|
-
// ── Types ──
|
|
26
|
-
|
|
27
|
-
#[derive(Debug, Serialize)]
|
|
28
|
-
pub struct PeriodEconomics {
|
|
29
|
-
pub label: String,
|
|
30
|
-
// ccusage metrics (Option for graceful degradation)
|
|
31
|
-
pub cc_cost: Option<f64>,
|
|
32
|
-
pub cc_total_tokens: Option<u64>,
|
|
33
|
-
pub cc_active_tokens: Option<u64>, // input + output only (excluding cache)
|
|
34
|
-
// Per-type token breakdown
|
|
35
|
-
pub cc_input_tokens: Option<u64>,
|
|
36
|
-
pub cc_output_tokens: Option<u64>,
|
|
37
|
-
pub cc_cache_create_tokens: Option<u64>,
|
|
38
|
-
pub cc_cache_read_tokens: Option<u64>,
|
|
39
|
-
// rtk metrics
|
|
40
|
-
pub rtk_commands: Option<usize>,
|
|
41
|
-
pub rtk_saved_tokens: Option<usize>,
|
|
42
|
-
pub rtk_savings_pct: Option<f64>,
|
|
43
|
-
// Primary metric (weighted input CPT)
|
|
44
|
-
pub weighted_input_cpt: Option<f64>, // Derived input CPT using API ratios
|
|
45
|
-
pub savings_weighted: Option<f64>, // saved * weighted_input_cpt (PRIMARY)
|
|
46
|
-
// Legacy metrics (verbose mode only)
|
|
47
|
-
pub blended_cpt: Option<f64>, // cost / total_tokens (diluted by cache)
|
|
48
|
-
pub active_cpt: Option<f64>, // cost / active_tokens (OVERESTIMATES)
|
|
49
|
-
pub savings_blended: Option<f64>, // saved * blended_cpt (UNDERESTIMATES)
|
|
50
|
-
pub savings_active: Option<f64>, // saved * active_cpt (OVERESTIMATES)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
impl PeriodEconomics {
|
|
54
|
-
fn new(label: &str) -> Self {
|
|
55
|
-
Self {
|
|
56
|
-
label: label.to_string(),
|
|
57
|
-
cc_cost: None,
|
|
58
|
-
cc_total_tokens: None,
|
|
59
|
-
cc_active_tokens: None,
|
|
60
|
-
cc_input_tokens: None,
|
|
61
|
-
cc_output_tokens: None,
|
|
62
|
-
cc_cache_create_tokens: None,
|
|
63
|
-
cc_cache_read_tokens: None,
|
|
64
|
-
rtk_commands: None,
|
|
65
|
-
rtk_saved_tokens: None,
|
|
66
|
-
rtk_savings_pct: None,
|
|
67
|
-
weighted_input_cpt: None,
|
|
68
|
-
savings_weighted: None,
|
|
69
|
-
blended_cpt: None,
|
|
70
|
-
active_cpt: None,
|
|
71
|
-
savings_blended: None,
|
|
72
|
-
savings_active: None,
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
fn set_ccusage(&mut self, metrics: &ccusage::CcusageMetrics) {
|
|
77
|
-
self.cc_cost = Some(metrics.total_cost);
|
|
78
|
-
self.cc_total_tokens = Some(metrics.total_tokens);
|
|
79
|
-
|
|
80
|
-
// Store per-type tokens
|
|
81
|
-
self.cc_input_tokens = Some(metrics.input_tokens);
|
|
82
|
-
self.cc_output_tokens = Some(metrics.output_tokens);
|
|
83
|
-
self.cc_cache_create_tokens = Some(metrics.cache_creation_tokens);
|
|
84
|
-
self.cc_cache_read_tokens = Some(metrics.cache_read_tokens);
|
|
85
|
-
|
|
86
|
-
// Active tokens (legacy)
|
|
87
|
-
let active = metrics.input_tokens + metrics.output_tokens;
|
|
88
|
-
self.cc_active_tokens = Some(active);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
fn set_rtk_from_day(&mut self, stats: &DayStats) {
|
|
92
|
-
self.rtk_commands = Some(stats.commands);
|
|
93
|
-
self.rtk_saved_tokens = Some(stats.saved_tokens);
|
|
94
|
-
self.rtk_savings_pct = Some(stats.savings_pct);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
fn set_rtk_from_week(&mut self, stats: &WeekStats) {
|
|
98
|
-
self.rtk_commands = Some(stats.commands);
|
|
99
|
-
self.rtk_saved_tokens = Some(stats.saved_tokens);
|
|
100
|
-
self.rtk_savings_pct = Some(stats.savings_pct);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
fn set_rtk_from_month(&mut self, stats: &MonthStats) {
|
|
104
|
-
self.rtk_commands = Some(stats.commands);
|
|
105
|
-
self.rtk_saved_tokens = Some(stats.saved_tokens);
|
|
106
|
-
self.rtk_savings_pct = Some(if stats.input_tokens + stats.output_tokens > 0 {
|
|
107
|
-
stats.saved_tokens as f64
|
|
108
|
-
/ (stats.saved_tokens + stats.input_tokens + stats.output_tokens) as f64
|
|
109
|
-
* 100.0
|
|
110
|
-
} else {
|
|
111
|
-
0.0
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
fn compute_weighted_metrics(&mut self) {
|
|
116
|
-
// Weighted input CPT derivation using API price ratios
|
|
117
|
-
if let (Some(cost), Some(saved)) = (self.cc_cost, self.rtk_saved_tokens) {
|
|
118
|
-
if let (Some(input), Some(output), Some(cache_create), Some(cache_read)) = (
|
|
119
|
-
self.cc_input_tokens,
|
|
120
|
-
self.cc_output_tokens,
|
|
121
|
-
self.cc_cache_create_tokens,
|
|
122
|
-
self.cc_cache_read_tokens,
|
|
123
|
-
) {
|
|
124
|
-
// Weighted units = input + 5*output + 1.25*cache_create + 0.1*cache_read
|
|
125
|
-
let weighted_units = input as f64
|
|
126
|
-
+ WEIGHT_OUTPUT * output as f64
|
|
127
|
-
+ WEIGHT_CACHE_CREATE * cache_create as f64
|
|
128
|
-
+ WEIGHT_CACHE_READ * cache_read as f64;
|
|
129
|
-
|
|
130
|
-
if weighted_units > 0.0 {
|
|
131
|
-
let input_cpt = cost / weighted_units;
|
|
132
|
-
let savings = saved as f64 * input_cpt;
|
|
133
|
-
|
|
134
|
-
self.weighted_input_cpt = Some(input_cpt);
|
|
135
|
-
self.savings_weighted = Some(savings);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
fn compute_dual_metrics(&mut self) {
|
|
142
|
-
if let (Some(cost), Some(saved)) = (self.cc_cost, self.rtk_saved_tokens) {
|
|
143
|
-
// Blended CPT (cost / total_tokens including cache)
|
|
144
|
-
if let Some(total) = self.cc_total_tokens {
|
|
145
|
-
if total > 0 {
|
|
146
|
-
self.blended_cpt = Some(cost / total as f64);
|
|
147
|
-
self.savings_blended = Some(saved as f64 * (cost / total as f64));
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Active CPT (cost / active_tokens = input+output only)
|
|
152
|
-
if let Some(active) = self.cc_active_tokens {
|
|
153
|
-
if active > 0 {
|
|
154
|
-
self.active_cpt = Some(cost / active as f64);
|
|
155
|
-
self.savings_active = Some(saved as f64 * (cost / active as f64));
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
#[derive(Debug, Serialize)]
|
|
163
|
-
struct Totals {
|
|
164
|
-
cc_cost: f64,
|
|
165
|
-
cc_total_tokens: u64,
|
|
166
|
-
cc_active_tokens: u64,
|
|
167
|
-
cc_input_tokens: u64,
|
|
168
|
-
cc_output_tokens: u64,
|
|
169
|
-
cc_cache_create_tokens: u64,
|
|
170
|
-
cc_cache_read_tokens: u64,
|
|
171
|
-
rtk_commands: usize,
|
|
172
|
-
rtk_saved_tokens: usize,
|
|
173
|
-
rtk_avg_savings_pct: f64,
|
|
174
|
-
weighted_input_cpt: Option<f64>,
|
|
175
|
-
savings_weighted: Option<f64>,
|
|
176
|
-
blended_cpt: Option<f64>,
|
|
177
|
-
active_cpt: Option<f64>,
|
|
178
|
-
savings_blended: Option<f64>,
|
|
179
|
-
savings_active: Option<f64>,
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ── Public API ──
|
|
183
|
-
|
|
184
|
-
pub fn run(
|
|
185
|
-
daily: bool,
|
|
186
|
-
weekly: bool,
|
|
187
|
-
monthly: bool,
|
|
188
|
-
all: bool,
|
|
189
|
-
format: &str,
|
|
190
|
-
verbose: u8,
|
|
191
|
-
) -> Result<()> {
|
|
192
|
-
let tracker = Tracker::new().context("Failed to initialize tracking database")?;
|
|
193
|
-
|
|
194
|
-
match format {
|
|
195
|
-
"json" => export_json(&tracker, daily, weekly, monthly, all),
|
|
196
|
-
"csv" => export_csv(&tracker, daily, weekly, monthly, all),
|
|
197
|
-
_ => display_text(&tracker, daily, weekly, monthly, all, verbose),
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// ── Merge Logic ──
|
|
202
|
-
|
|
203
|
-
fn merge_daily(cc: Option<Vec<CcusagePeriod>>, rtk: Vec<DayStats>) -> Vec<PeriodEconomics> {
|
|
204
|
-
let mut map: HashMap<String, PeriodEconomics> = HashMap::new();
|
|
205
|
-
|
|
206
|
-
// Insert ccusage data
|
|
207
|
-
if let Some(cc_data) = cc {
|
|
208
|
-
for entry in cc_data {
|
|
209
|
-
let crate::ccusage::CcusagePeriod { key, metrics } = entry;
|
|
210
|
-
map.entry(key)
|
|
211
|
-
.or_insert_with_key(|k| PeriodEconomics::new(k))
|
|
212
|
-
.set_ccusage(&metrics);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Merge rtk data
|
|
217
|
-
for entry in rtk {
|
|
218
|
-
map.entry(entry.date.clone())
|
|
219
|
-
.or_insert_with_key(|k| PeriodEconomics::new(k))
|
|
220
|
-
.set_rtk_from_day(&entry);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Compute dual metrics and sort
|
|
224
|
-
let mut result: Vec<_> = map.into_values().collect();
|
|
225
|
-
for period in &mut result {
|
|
226
|
-
period.compute_weighted_metrics();
|
|
227
|
-
period.compute_dual_metrics();
|
|
228
|
-
}
|
|
229
|
-
result.sort_by(|a, b| a.label.cmp(&b.label));
|
|
230
|
-
result
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
fn merge_weekly(cc: Option<Vec<CcusagePeriod>>, rtk: Vec<WeekStats>) -> Vec<PeriodEconomics> {
|
|
234
|
-
let mut map: HashMap<String, PeriodEconomics> = HashMap::new();
|
|
235
|
-
|
|
236
|
-
// Insert ccusage data (key = ISO Monday "2026-01-20")
|
|
237
|
-
if let Some(cc_data) = cc {
|
|
238
|
-
for entry in cc_data {
|
|
239
|
-
let crate::ccusage::CcusagePeriod { key, metrics } = entry;
|
|
240
|
-
map.entry(key)
|
|
241
|
-
.or_insert_with_key(|k| PeriodEconomics::new(k))
|
|
242
|
-
.set_ccusage(&metrics);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Merge rtk data (week_start = legacy Saturday "2026-01-18")
|
|
247
|
-
// Convert Saturday to Monday for alignment
|
|
248
|
-
for entry in rtk {
|
|
249
|
-
let monday_key = match convert_saturday_to_monday(&entry.week_start) {
|
|
250
|
-
Some(m) => m,
|
|
251
|
-
None => {
|
|
252
|
-
eprintln!("⚠️ Invalid week_start format: {}", entry.week_start);
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
map.entry(monday_key)
|
|
258
|
-
.or_insert_with_key(|key| PeriodEconomics::new(key))
|
|
259
|
-
.set_rtk_from_week(&entry);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
let mut result: Vec<_> = map.into_values().collect();
|
|
263
|
-
for period in &mut result {
|
|
264
|
-
period.compute_weighted_metrics();
|
|
265
|
-
period.compute_dual_metrics();
|
|
266
|
-
}
|
|
267
|
-
result.sort_by(|a, b| a.label.cmp(&b.label));
|
|
268
|
-
result
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
fn merge_monthly(cc: Option<Vec<CcusagePeriod>>, rtk: Vec<MonthStats>) -> Vec<PeriodEconomics> {
|
|
272
|
-
let mut map: HashMap<String, PeriodEconomics> = HashMap::new();
|
|
273
|
-
|
|
274
|
-
// Insert ccusage data
|
|
275
|
-
if let Some(cc_data) = cc {
|
|
276
|
-
for entry in cc_data {
|
|
277
|
-
let crate::ccusage::CcusagePeriod { key, metrics } = entry;
|
|
278
|
-
map.entry(key)
|
|
279
|
-
.or_insert_with_key(|k| PeriodEconomics::new(k))
|
|
280
|
-
.set_ccusage(&metrics);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Merge rtk data
|
|
285
|
-
for entry in rtk {
|
|
286
|
-
map.entry(entry.month.clone())
|
|
287
|
-
.or_insert_with_key(|k| PeriodEconomics::new(k))
|
|
288
|
-
.set_rtk_from_month(&entry);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
let mut result: Vec<_> = map.into_values().collect();
|
|
292
|
-
for period in &mut result {
|
|
293
|
-
period.compute_weighted_metrics();
|
|
294
|
-
period.compute_dual_metrics();
|
|
295
|
-
}
|
|
296
|
-
result.sort_by(|a, b| a.label.cmp(&b.label));
|
|
297
|
-
result
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── Helpers ──
|
|
301
|
-
|
|
302
|
-
/// Convert Saturday week_start (legacy rtk) to ISO Monday
|
|
303
|
-
/// Example: "2026-01-18" (Sat) -> "2026-01-20" (Mon)
|
|
304
|
-
fn convert_saturday_to_monday(saturday: &str) -> Option<String> {
|
|
305
|
-
let sat_date = NaiveDate::parse_from_str(saturday, "%Y-%m-%d").ok()?;
|
|
306
|
-
|
|
307
|
-
// rtk uses Saturday as week start, ISO uses Monday
|
|
308
|
-
// Saturday + 2 days = Monday
|
|
309
|
-
let monday = sat_date + chrono::TimeDelta::try_days(2)?;
|
|
310
|
-
|
|
311
|
-
Some(monday.format("%Y-%m-%d").to_string())
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
fn compute_totals(periods: &[PeriodEconomics]) -> Totals {
|
|
315
|
-
let mut totals = Totals {
|
|
316
|
-
cc_cost: 0.0,
|
|
317
|
-
cc_total_tokens: 0,
|
|
318
|
-
cc_active_tokens: 0,
|
|
319
|
-
cc_input_tokens: 0,
|
|
320
|
-
cc_output_tokens: 0,
|
|
321
|
-
cc_cache_create_tokens: 0,
|
|
322
|
-
cc_cache_read_tokens: 0,
|
|
323
|
-
rtk_commands: 0,
|
|
324
|
-
rtk_saved_tokens: 0,
|
|
325
|
-
rtk_avg_savings_pct: 0.0,
|
|
326
|
-
weighted_input_cpt: None,
|
|
327
|
-
savings_weighted: None,
|
|
328
|
-
blended_cpt: None,
|
|
329
|
-
active_cpt: None,
|
|
330
|
-
savings_blended: None,
|
|
331
|
-
savings_active: None,
|
|
332
|
-
};
|
|
333
|
-
|
|
334
|
-
let mut pct_sum = 0.0;
|
|
335
|
-
let mut pct_count = 0;
|
|
336
|
-
|
|
337
|
-
for p in periods {
|
|
338
|
-
if let Some(cost) = p.cc_cost {
|
|
339
|
-
totals.cc_cost += cost;
|
|
340
|
-
}
|
|
341
|
-
if let Some(total) = p.cc_total_tokens {
|
|
342
|
-
totals.cc_total_tokens += total;
|
|
343
|
-
}
|
|
344
|
-
if let Some(active) = p.cc_active_tokens {
|
|
345
|
-
totals.cc_active_tokens += active;
|
|
346
|
-
}
|
|
347
|
-
if let Some(input) = p.cc_input_tokens {
|
|
348
|
-
totals.cc_input_tokens += input;
|
|
349
|
-
}
|
|
350
|
-
if let Some(output) = p.cc_output_tokens {
|
|
351
|
-
totals.cc_output_tokens += output;
|
|
352
|
-
}
|
|
353
|
-
if let Some(cache_create) = p.cc_cache_create_tokens {
|
|
354
|
-
totals.cc_cache_create_tokens += cache_create;
|
|
355
|
-
}
|
|
356
|
-
if let Some(cache_read) = p.cc_cache_read_tokens {
|
|
357
|
-
totals.cc_cache_read_tokens += cache_read;
|
|
358
|
-
}
|
|
359
|
-
if let Some(cmds) = p.rtk_commands {
|
|
360
|
-
totals.rtk_commands += cmds;
|
|
361
|
-
}
|
|
362
|
-
if let Some(saved) = p.rtk_saved_tokens {
|
|
363
|
-
totals.rtk_saved_tokens += saved;
|
|
364
|
-
}
|
|
365
|
-
if let Some(pct) = p.rtk_savings_pct {
|
|
366
|
-
pct_sum += pct;
|
|
367
|
-
pct_count += 1;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if pct_count > 0 {
|
|
372
|
-
totals.rtk_avg_savings_pct = pct_sum / pct_count as f64;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Compute global weighted metrics
|
|
376
|
-
let weighted_units = totals.cc_input_tokens as f64
|
|
377
|
-
+ WEIGHT_OUTPUT * totals.cc_output_tokens as f64
|
|
378
|
-
+ WEIGHT_CACHE_CREATE * totals.cc_cache_create_tokens as f64
|
|
379
|
-
+ WEIGHT_CACHE_READ * totals.cc_cache_read_tokens as f64;
|
|
380
|
-
|
|
381
|
-
if weighted_units > 0.0 {
|
|
382
|
-
let input_cpt = totals.cc_cost / weighted_units;
|
|
383
|
-
totals.weighted_input_cpt = Some(input_cpt);
|
|
384
|
-
totals.savings_weighted = Some(totals.rtk_saved_tokens as f64 * input_cpt);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Compute global dual metrics (legacy)
|
|
388
|
-
if totals.cc_total_tokens > 0 {
|
|
389
|
-
totals.blended_cpt = Some(totals.cc_cost / totals.cc_total_tokens as f64);
|
|
390
|
-
totals.savings_blended = Some(totals.rtk_saved_tokens as f64 * totals.blended_cpt.unwrap());
|
|
391
|
-
}
|
|
392
|
-
if totals.cc_active_tokens > 0 {
|
|
393
|
-
totals.active_cpt = Some(totals.cc_cost / totals.cc_active_tokens as f64);
|
|
394
|
-
totals.savings_active = Some(totals.rtk_saved_tokens as f64 * totals.active_cpt.unwrap());
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
totals
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// ── Display ──
|
|
401
|
-
|
|
402
|
-
fn display_text(
|
|
403
|
-
tracker: &Tracker,
|
|
404
|
-
daily: bool,
|
|
405
|
-
weekly: bool,
|
|
406
|
-
monthly: bool,
|
|
407
|
-
all: bool,
|
|
408
|
-
verbose: u8,
|
|
409
|
-
) -> Result<()> {
|
|
410
|
-
// Default: summary view
|
|
411
|
-
if !daily && !weekly && !monthly && !all {
|
|
412
|
-
display_summary(tracker, verbose)?;
|
|
413
|
-
return Ok(());
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if all || daily {
|
|
417
|
-
display_daily(tracker, verbose)?;
|
|
418
|
-
}
|
|
419
|
-
if all || weekly {
|
|
420
|
-
display_weekly(tracker, verbose)?;
|
|
421
|
-
}
|
|
422
|
-
if all || monthly {
|
|
423
|
-
display_monthly(tracker, verbose)?;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
Ok(())
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
fn display_summary(tracker: &Tracker, verbose: u8) -> Result<()> {
|
|
430
|
-
let cc_monthly =
|
|
431
|
-
ccusage::fetch(Granularity::Monthly).context("Failed to fetch ccusage monthly data")?;
|
|
432
|
-
let rtk_monthly = tracker
|
|
433
|
-
.get_by_month()
|
|
434
|
-
.context("Failed to load monthly token savings from database")?;
|
|
435
|
-
let periods = merge_monthly(cc_monthly, rtk_monthly);
|
|
436
|
-
|
|
437
|
-
if periods.is_empty() {
|
|
438
|
-
println!("No data available. Run some rtk commands to start tracking.");
|
|
439
|
-
return Ok(());
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
let totals = compute_totals(&periods);
|
|
443
|
-
|
|
444
|
-
println!("💰 Claude Code Economics");
|
|
445
|
-
println!("════════════════════════════════════════════════════");
|
|
446
|
-
println!();
|
|
447
|
-
|
|
448
|
-
println!(
|
|
449
|
-
" Spent (ccusage): {}",
|
|
450
|
-
format_usd(totals.cc_cost)
|
|
451
|
-
);
|
|
452
|
-
println!(" Token breakdown:");
|
|
453
|
-
println!(
|
|
454
|
-
" Input: {}",
|
|
455
|
-
format_tokens(totals.cc_input_tokens as usize)
|
|
456
|
-
);
|
|
457
|
-
println!(
|
|
458
|
-
" Output: {}",
|
|
459
|
-
format_tokens(totals.cc_output_tokens as usize)
|
|
460
|
-
);
|
|
461
|
-
println!(
|
|
462
|
-
" Cache writes: {}",
|
|
463
|
-
format_tokens(totals.cc_cache_create_tokens as usize)
|
|
464
|
-
);
|
|
465
|
-
println!(
|
|
466
|
-
" Cache reads: {}",
|
|
467
|
-
format_tokens(totals.cc_cache_read_tokens as usize)
|
|
468
|
-
);
|
|
469
|
-
println!();
|
|
470
|
-
|
|
471
|
-
println!(" RTK commands: {}", totals.rtk_commands);
|
|
472
|
-
println!(
|
|
473
|
-
" Tokens saved: {}",
|
|
474
|
-
format_tokens(totals.rtk_saved_tokens)
|
|
475
|
-
);
|
|
476
|
-
println!();
|
|
477
|
-
|
|
478
|
-
println!(" Estimated Savings:");
|
|
479
|
-
println!(" ┌─────────────────────────────────────────────────┐");
|
|
480
|
-
|
|
481
|
-
if let Some(weighted_savings) = totals.savings_weighted {
|
|
482
|
-
let weighted_pct = if totals.cc_cost > 0.0 {
|
|
483
|
-
(weighted_savings / totals.cc_cost) * 100.0
|
|
484
|
-
} else {
|
|
485
|
-
0.0
|
|
486
|
-
};
|
|
487
|
-
println!(
|
|
488
|
-
" │ Input token pricing: {} ({:.1}%) │",
|
|
489
|
-
format_usd(weighted_savings).trim_end(),
|
|
490
|
-
weighted_pct
|
|
491
|
-
);
|
|
492
|
-
if let Some(input_cpt) = totals.weighted_input_cpt {
|
|
493
|
-
println!(
|
|
494
|
-
" │ Derived input CPT: {} │",
|
|
495
|
-
format_cpt(input_cpt)
|
|
496
|
-
);
|
|
497
|
-
}
|
|
498
|
-
} else {
|
|
499
|
-
println!(" │ Input token pricing: — │");
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
println!(" └─────────────────────────────────────────────────┘");
|
|
503
|
-
println!();
|
|
504
|
-
|
|
505
|
-
println!(" How it works:");
|
|
506
|
-
println!(" RTK compresses CLI outputs before they enter Claude's context.");
|
|
507
|
-
println!(" Savings derived using API price ratios (out=5x, cache_w=1.25x, cache_r=0.1x).");
|
|
508
|
-
println!();
|
|
509
|
-
|
|
510
|
-
// Verbose mode: legacy metrics
|
|
511
|
-
if verbose > 0 {
|
|
512
|
-
println!(" Legacy metrics (reference only):");
|
|
513
|
-
if let Some(active_savings) = totals.savings_active {
|
|
514
|
-
let active_pct = if totals.cc_cost > 0.0 {
|
|
515
|
-
(active_savings / totals.cc_cost) * 100.0
|
|
516
|
-
} else {
|
|
517
|
-
0.0
|
|
518
|
-
};
|
|
519
|
-
println!(
|
|
520
|
-
" Active (OVERESTIMATES): {} ({:.1}%)",
|
|
521
|
-
format_usd(active_savings),
|
|
522
|
-
active_pct
|
|
523
|
-
);
|
|
524
|
-
}
|
|
525
|
-
if let Some(blended_savings) = totals.savings_blended {
|
|
526
|
-
let blended_pct = if totals.cc_cost > 0.0 {
|
|
527
|
-
(blended_savings / totals.cc_cost) * 100.0
|
|
528
|
-
} else {
|
|
529
|
-
0.0
|
|
530
|
-
};
|
|
531
|
-
println!(
|
|
532
|
-
" Blended (UNDERESTIMATES): {} ({:.2}%)",
|
|
533
|
-
format_usd(blended_savings),
|
|
534
|
-
blended_pct
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
println!(" Note: Saved tokens estimated via chars/4 heuristic, not exact tokenizer.");
|
|
538
|
-
println!();
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
Ok(())
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
fn display_daily(tracker: &Tracker, verbose: u8) -> Result<()> {
|
|
545
|
-
let cc_daily =
|
|
546
|
-
ccusage::fetch(Granularity::Daily).context("Failed to fetch ccusage daily data")?;
|
|
547
|
-
let rtk_daily = tracker
|
|
548
|
-
.get_all_days()
|
|
549
|
-
.context("Failed to load daily token savings from database")?;
|
|
550
|
-
let periods = merge_daily(cc_daily, rtk_daily);
|
|
551
|
-
|
|
552
|
-
println!("📅 Daily Economics");
|
|
553
|
-
println!("════════════════════════════════════════════════════");
|
|
554
|
-
print_period_table(&periods, verbose);
|
|
555
|
-
Ok(())
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
fn display_weekly(tracker: &Tracker, verbose: u8) -> Result<()> {
|
|
559
|
-
let cc_weekly =
|
|
560
|
-
ccusage::fetch(Granularity::Weekly).context("Failed to fetch ccusage weekly data")?;
|
|
561
|
-
let rtk_weekly = tracker
|
|
562
|
-
.get_by_week()
|
|
563
|
-
.context("Failed to load weekly token savings from database")?;
|
|
564
|
-
let periods = merge_weekly(cc_weekly, rtk_weekly);
|
|
565
|
-
|
|
566
|
-
println!("📅 Weekly Economics");
|
|
567
|
-
println!("════════════════════════════════════════════════════");
|
|
568
|
-
print_period_table(&periods, verbose);
|
|
569
|
-
Ok(())
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
fn display_monthly(tracker: &Tracker, verbose: u8) -> Result<()> {
|
|
573
|
-
let cc_monthly =
|
|
574
|
-
ccusage::fetch(Granularity::Monthly).context("Failed to fetch ccusage monthly data")?;
|
|
575
|
-
let rtk_monthly = tracker
|
|
576
|
-
.get_by_month()
|
|
577
|
-
.context("Failed to load monthly token savings from database")?;
|
|
578
|
-
let periods = merge_monthly(cc_monthly, rtk_monthly);
|
|
579
|
-
|
|
580
|
-
println!("📅 Monthly Economics");
|
|
581
|
-
println!("════════════════════════════════════════════════════");
|
|
582
|
-
print_period_table(&periods, verbose);
|
|
583
|
-
Ok(())
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
fn print_period_table(periods: &[PeriodEconomics], verbose: u8) {
|
|
587
|
-
println!();
|
|
588
|
-
|
|
589
|
-
if verbose > 0 {
|
|
590
|
-
// Verbose: include legacy metrics
|
|
591
|
-
println!(
|
|
592
|
-
"{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12}",
|
|
593
|
-
"Period", "Spent", "Saved", "Savings", "Active$", "Blended$", "RTK Cmds"
|
|
594
|
-
);
|
|
595
|
-
println!(
|
|
596
|
-
"{:-<12} {:-<10} {:-<10} {:-<10} {:-<10} {:-<12} {:-<12}",
|
|
597
|
-
"", "", "", "", "", "", ""
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
for p in periods {
|
|
601
|
-
let spent = p.cc_cost.map(format_usd).unwrap_or_else(|| "—".to_string());
|
|
602
|
-
let saved = p
|
|
603
|
-
.rtk_saved_tokens
|
|
604
|
-
.map(format_tokens)
|
|
605
|
-
.unwrap_or_else(|| "—".to_string());
|
|
606
|
-
let weighted = p
|
|
607
|
-
.savings_weighted
|
|
608
|
-
.map(format_usd)
|
|
609
|
-
.unwrap_or_else(|| "—".to_string());
|
|
610
|
-
let active = p
|
|
611
|
-
.savings_active
|
|
612
|
-
.map(format_usd)
|
|
613
|
-
.unwrap_or_else(|| "—".to_string());
|
|
614
|
-
let blended = p
|
|
615
|
-
.savings_blended
|
|
616
|
-
.map(format_usd)
|
|
617
|
-
.unwrap_or_else(|| "—".to_string());
|
|
618
|
-
let cmds = p
|
|
619
|
-
.rtk_commands
|
|
620
|
-
.map(|c| c.to_string())
|
|
621
|
-
.unwrap_or_else(|| "—".to_string());
|
|
622
|
-
|
|
623
|
-
println!(
|
|
624
|
-
"{:<12} {:>10} {:>10} {:>10} {:>10} {:>12} {:>12}",
|
|
625
|
-
p.label, spent, saved, weighted, active, blended, cmds
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
} else {
|
|
629
|
-
// Default: single Savings column
|
|
630
|
-
println!(
|
|
631
|
-
"{:<12} {:>10} {:>10} {:>10} {:>12}",
|
|
632
|
-
"Period", "Spent", "Saved", "Savings", "RTK Cmds"
|
|
633
|
-
);
|
|
634
|
-
println!(
|
|
635
|
-
"{:-<12} {:-<10} {:-<10} {:-<10} {:-<12}",
|
|
636
|
-
"", "", "", "", ""
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
for p in periods {
|
|
640
|
-
let spent = p.cc_cost.map(format_usd).unwrap_or_else(|| "—".to_string());
|
|
641
|
-
let saved = p
|
|
642
|
-
.rtk_saved_tokens
|
|
643
|
-
.map(format_tokens)
|
|
644
|
-
.unwrap_or_else(|| "—".to_string());
|
|
645
|
-
let weighted = p
|
|
646
|
-
.savings_weighted
|
|
647
|
-
.map(format_usd)
|
|
648
|
-
.unwrap_or_else(|| "—".to_string());
|
|
649
|
-
let cmds = p
|
|
650
|
-
.rtk_commands
|
|
651
|
-
.map(|c| c.to_string())
|
|
652
|
-
.unwrap_or_else(|| "—".to_string());
|
|
653
|
-
|
|
654
|
-
println!(
|
|
655
|
-
"{:<12} {:>10} {:>10} {:>10} {:>12}",
|
|
656
|
-
p.label, spent, saved, weighted, cmds
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
println!();
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// ── Export ──
|
|
664
|
-
|
|
665
|
-
fn export_json(
|
|
666
|
-
tracker: &Tracker,
|
|
667
|
-
daily: bool,
|
|
668
|
-
weekly: bool,
|
|
669
|
-
monthly: bool,
|
|
670
|
-
all: bool,
|
|
671
|
-
) -> Result<()> {
|
|
672
|
-
#[derive(Serialize)]
|
|
673
|
-
struct Export {
|
|
674
|
-
daily: Option<Vec<PeriodEconomics>>,
|
|
675
|
-
weekly: Option<Vec<PeriodEconomics>>,
|
|
676
|
-
monthly: Option<Vec<PeriodEconomics>>,
|
|
677
|
-
totals: Option<Totals>,
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
let mut export = Export {
|
|
681
|
-
daily: None,
|
|
682
|
-
weekly: None,
|
|
683
|
-
monthly: None,
|
|
684
|
-
totals: None,
|
|
685
|
-
};
|
|
686
|
-
|
|
687
|
-
if all || daily {
|
|
688
|
-
let cc = ccusage::fetch(Granularity::Daily)
|
|
689
|
-
.context("Failed to fetch ccusage daily data for JSON export")?;
|
|
690
|
-
let rtk = tracker
|
|
691
|
-
.get_all_days()
|
|
692
|
-
.context("Failed to load daily token savings for JSON export")?;
|
|
693
|
-
export.daily = Some(merge_daily(cc, rtk));
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
if all || weekly {
|
|
697
|
-
let cc = ccusage::fetch(Granularity::Weekly)
|
|
698
|
-
.context("Failed to fetch ccusage weekly data for export")?;
|
|
699
|
-
let rtk = tracker
|
|
700
|
-
.get_by_week()
|
|
701
|
-
.context("Failed to load weekly token savings for export")?;
|
|
702
|
-
export.weekly = Some(merge_weekly(cc, rtk));
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if all || monthly {
|
|
706
|
-
let cc = ccusage::fetch(Granularity::Monthly)
|
|
707
|
-
.context("Failed to fetch ccusage monthly data for export")?;
|
|
708
|
-
let rtk = tracker
|
|
709
|
-
.get_by_month()
|
|
710
|
-
.context("Failed to load monthly token savings for export")?;
|
|
711
|
-
let periods = merge_monthly(cc, rtk);
|
|
712
|
-
export.totals = Some(compute_totals(&periods));
|
|
713
|
-
export.monthly = Some(periods);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
println!(
|
|
717
|
-
"{}",
|
|
718
|
-
serde_json::to_string_pretty(&export)
|
|
719
|
-
.context("Failed to serialize economics data to JSON")?
|
|
720
|
-
);
|
|
721
|
-
Ok(())
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
fn export_csv(
|
|
725
|
-
tracker: &Tracker,
|
|
726
|
-
daily: bool,
|
|
727
|
-
weekly: bool,
|
|
728
|
-
monthly: bool,
|
|
729
|
-
all: bool,
|
|
730
|
-
) -> Result<()> {
|
|
731
|
-
// Header (new columns: input_tokens, output_tokens, cache_create, cache_read, weighted_savings)
|
|
732
|
-
println!("period,spent,input_tokens,output_tokens,cache_create,cache_read,active_tokens,total_tokens,saved_tokens,weighted_savings,active_savings,blended_savings,rtk_commands");
|
|
733
|
-
|
|
734
|
-
if all || daily {
|
|
735
|
-
let cc = ccusage::fetch(Granularity::Daily)
|
|
736
|
-
.context("Failed to fetch ccusage daily data for JSON export")?;
|
|
737
|
-
let rtk = tracker
|
|
738
|
-
.get_all_days()
|
|
739
|
-
.context("Failed to load daily token savings for JSON export")?;
|
|
740
|
-
let periods = merge_daily(cc, rtk);
|
|
741
|
-
for p in periods {
|
|
742
|
-
print_csv_row(&p);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
if all || weekly {
|
|
747
|
-
let cc = ccusage::fetch(Granularity::Weekly)
|
|
748
|
-
.context("Failed to fetch ccusage weekly data for export")?;
|
|
749
|
-
let rtk = tracker
|
|
750
|
-
.get_by_week()
|
|
751
|
-
.context("Failed to load weekly token savings for export")?;
|
|
752
|
-
let periods = merge_weekly(cc, rtk);
|
|
753
|
-
for p in periods {
|
|
754
|
-
print_csv_row(&p);
|
|
755
|
-
}
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
if all || monthly {
|
|
759
|
-
let cc = ccusage::fetch(Granularity::Monthly)
|
|
760
|
-
.context("Failed to fetch ccusage monthly data for export")?;
|
|
761
|
-
let rtk = tracker
|
|
762
|
-
.get_by_month()
|
|
763
|
-
.context("Failed to load monthly token savings for export")?;
|
|
764
|
-
let periods = merge_monthly(cc, rtk);
|
|
765
|
-
for p in periods {
|
|
766
|
-
print_csv_row(&p);
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
Ok(())
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
fn print_csv_row(p: &PeriodEconomics) {
|
|
774
|
-
let spent = p.cc_cost.map(|c| format!("{:.4}", c)).unwrap_or_default();
|
|
775
|
-
let input_tokens = p.cc_input_tokens.map(|t| t.to_string()).unwrap_or_default();
|
|
776
|
-
let output_tokens = p
|
|
777
|
-
.cc_output_tokens
|
|
778
|
-
.map(|t| t.to_string())
|
|
779
|
-
.unwrap_or_default();
|
|
780
|
-
let cache_create = p
|
|
781
|
-
.cc_cache_create_tokens
|
|
782
|
-
.map(|t| t.to_string())
|
|
783
|
-
.unwrap_or_default();
|
|
784
|
-
let cache_read = p
|
|
785
|
-
.cc_cache_read_tokens
|
|
786
|
-
.map(|t| t.to_string())
|
|
787
|
-
.unwrap_or_default();
|
|
788
|
-
let active_tokens = p
|
|
789
|
-
.cc_active_tokens
|
|
790
|
-
.map(|t| t.to_string())
|
|
791
|
-
.unwrap_or_default();
|
|
792
|
-
let total_tokens = p.cc_total_tokens.map(|t| t.to_string()).unwrap_or_default();
|
|
793
|
-
let saved_tokens = p
|
|
794
|
-
.rtk_saved_tokens
|
|
795
|
-
.map(|t| t.to_string())
|
|
796
|
-
.unwrap_or_default();
|
|
797
|
-
let weighted_savings = p
|
|
798
|
-
.savings_weighted
|
|
799
|
-
.map(|s| format!("{:.4}", s))
|
|
800
|
-
.unwrap_or_default();
|
|
801
|
-
let active_savings = p
|
|
802
|
-
.savings_active
|
|
803
|
-
.map(|s| format!("{:.4}", s))
|
|
804
|
-
.unwrap_or_default();
|
|
805
|
-
let blended_savings = p
|
|
806
|
-
.savings_blended
|
|
807
|
-
.map(|s| format!("{:.4}", s))
|
|
808
|
-
.unwrap_or_default();
|
|
809
|
-
let cmds = p.rtk_commands.map(|c| c.to_string()).unwrap_or_default();
|
|
810
|
-
|
|
811
|
-
println!(
|
|
812
|
-
"{},{},{},{},{},{},{},{},{},{},{},{},{}",
|
|
813
|
-
p.label,
|
|
814
|
-
spent,
|
|
815
|
-
input_tokens,
|
|
816
|
-
output_tokens,
|
|
817
|
-
cache_create,
|
|
818
|
-
cache_read,
|
|
819
|
-
active_tokens,
|
|
820
|
-
total_tokens,
|
|
821
|
-
saved_tokens,
|
|
822
|
-
weighted_savings,
|
|
823
|
-
active_savings,
|
|
824
|
-
blended_savings,
|
|
825
|
-
cmds
|
|
826
|
-
);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
#[cfg(test)]
|
|
830
|
-
mod tests {
|
|
831
|
-
use super::*;
|
|
832
|
-
|
|
833
|
-
#[test]
|
|
834
|
-
fn test_convert_saturday_to_monday() {
|
|
835
|
-
// Saturday Jan 18 -> Monday Jan 20
|
|
836
|
-
assert_eq!(
|
|
837
|
-
convert_saturday_to_monday("2026-01-18"),
|
|
838
|
-
Some("2026-01-20".to_string())
|
|
839
|
-
);
|
|
840
|
-
|
|
841
|
-
// Invalid format
|
|
842
|
-
assert_eq!(convert_saturday_to_monday("invalid"), None);
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
#[test]
|
|
846
|
-
fn test_period_economics_new() {
|
|
847
|
-
let p = PeriodEconomics::new("2026-01");
|
|
848
|
-
assert_eq!(p.label, "2026-01");
|
|
849
|
-
assert!(p.cc_cost.is_none());
|
|
850
|
-
assert!(p.rtk_commands.is_none());
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
#[test]
|
|
854
|
-
fn test_compute_dual_metrics_with_data() {
|
|
855
|
-
let mut p = PeriodEconomics {
|
|
856
|
-
label: "2026-01".to_string(),
|
|
857
|
-
cc_cost: Some(100.0),
|
|
858
|
-
cc_total_tokens: Some(1_000_000),
|
|
859
|
-
cc_active_tokens: Some(10_000),
|
|
860
|
-
rtk_saved_tokens: Some(5_000),
|
|
861
|
-
..PeriodEconomics::new("2026-01")
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
p.compute_dual_metrics();
|
|
865
|
-
|
|
866
|
-
assert!(p.blended_cpt.is_some());
|
|
867
|
-
assert_eq!(p.blended_cpt.unwrap(), 100.0 / 1_000_000.0);
|
|
868
|
-
|
|
869
|
-
assert!(p.active_cpt.is_some());
|
|
870
|
-
assert_eq!(p.active_cpt.unwrap(), 100.0 / 10_000.0);
|
|
871
|
-
|
|
872
|
-
assert!(p.savings_blended.is_some());
|
|
873
|
-
assert!(p.savings_active.is_some());
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
#[test]
|
|
877
|
-
fn test_compute_dual_metrics_zero_tokens() {
|
|
878
|
-
let mut p = PeriodEconomics {
|
|
879
|
-
label: "2026-01".to_string(),
|
|
880
|
-
cc_cost: Some(100.0),
|
|
881
|
-
cc_total_tokens: Some(0),
|
|
882
|
-
cc_active_tokens: Some(0),
|
|
883
|
-
rtk_saved_tokens: Some(5_000),
|
|
884
|
-
..PeriodEconomics::new("2026-01")
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
p.compute_dual_metrics();
|
|
888
|
-
|
|
889
|
-
assert!(p.blended_cpt.is_none());
|
|
890
|
-
assert!(p.active_cpt.is_none());
|
|
891
|
-
assert!(p.savings_blended.is_none());
|
|
892
|
-
assert!(p.savings_active.is_none());
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
#[test]
|
|
896
|
-
fn test_compute_dual_metrics_no_ccusage_data() {
|
|
897
|
-
let mut p = PeriodEconomics {
|
|
898
|
-
label: "2026-01".to_string(),
|
|
899
|
-
rtk_saved_tokens: Some(5_000),
|
|
900
|
-
..PeriodEconomics::new("2026-01")
|
|
901
|
-
};
|
|
902
|
-
|
|
903
|
-
p.compute_dual_metrics();
|
|
904
|
-
|
|
905
|
-
assert!(p.blended_cpt.is_none());
|
|
906
|
-
assert!(p.active_cpt.is_none());
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
#[test]
|
|
910
|
-
fn test_merge_monthly_both_present() {
|
|
911
|
-
let cc = vec![CcusagePeriod {
|
|
912
|
-
key: "2026-01".to_string(),
|
|
913
|
-
metrics: ccusage::CcusageMetrics {
|
|
914
|
-
input_tokens: 1000,
|
|
915
|
-
output_tokens: 500,
|
|
916
|
-
cache_creation_tokens: 100,
|
|
917
|
-
cache_read_tokens: 200,
|
|
918
|
-
total_tokens: 1800,
|
|
919
|
-
total_cost: 12.34,
|
|
920
|
-
},
|
|
921
|
-
}];
|
|
922
|
-
|
|
923
|
-
let rtk = vec![MonthStats {
|
|
924
|
-
month: "2026-01".to_string(),
|
|
925
|
-
commands: 10,
|
|
926
|
-
input_tokens: 800,
|
|
927
|
-
output_tokens: 400,
|
|
928
|
-
saved_tokens: 5000,
|
|
929
|
-
savings_pct: 50.0,
|
|
930
|
-
total_time_ms: 0,
|
|
931
|
-
avg_time_ms: 0,
|
|
932
|
-
}];
|
|
933
|
-
|
|
934
|
-
let merged = merge_monthly(Some(cc), rtk);
|
|
935
|
-
assert_eq!(merged.len(), 1);
|
|
936
|
-
assert_eq!(merged[0].label, "2026-01");
|
|
937
|
-
assert_eq!(merged[0].cc_cost, Some(12.34));
|
|
938
|
-
assert_eq!(merged[0].rtk_commands, Some(10));
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
#[test]
|
|
942
|
-
fn test_merge_monthly_only_ccusage() {
|
|
943
|
-
let cc = vec![CcusagePeriod {
|
|
944
|
-
key: "2026-01".to_string(),
|
|
945
|
-
metrics: ccusage::CcusageMetrics {
|
|
946
|
-
input_tokens: 1000,
|
|
947
|
-
output_tokens: 500,
|
|
948
|
-
cache_creation_tokens: 100,
|
|
949
|
-
cache_read_tokens: 200,
|
|
950
|
-
total_tokens: 1800,
|
|
951
|
-
total_cost: 12.34,
|
|
952
|
-
},
|
|
953
|
-
}];
|
|
954
|
-
|
|
955
|
-
let merged = merge_monthly(Some(cc), vec![]);
|
|
956
|
-
assert_eq!(merged.len(), 1);
|
|
957
|
-
assert_eq!(merged[0].cc_cost, Some(12.34));
|
|
958
|
-
assert!(merged[0].rtk_commands.is_none());
|
|
959
|
-
}
|
|
960
|
-
|
|
961
|
-
#[test]
|
|
962
|
-
fn test_merge_monthly_only_rtk() {
|
|
963
|
-
let rtk = vec![MonthStats {
|
|
964
|
-
month: "2026-01".to_string(),
|
|
965
|
-
commands: 10,
|
|
966
|
-
input_tokens: 800,
|
|
967
|
-
output_tokens: 400,
|
|
968
|
-
saved_tokens: 5000,
|
|
969
|
-
savings_pct: 50.0,
|
|
970
|
-
total_time_ms: 0,
|
|
971
|
-
avg_time_ms: 0,
|
|
972
|
-
}];
|
|
973
|
-
|
|
974
|
-
let merged = merge_monthly(None, rtk);
|
|
975
|
-
assert_eq!(merged.len(), 1);
|
|
976
|
-
assert!(merged[0].cc_cost.is_none());
|
|
977
|
-
assert_eq!(merged[0].rtk_commands, Some(10));
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
#[test]
|
|
981
|
-
fn test_merge_monthly_sorted() {
|
|
982
|
-
let rtk = vec![
|
|
983
|
-
MonthStats {
|
|
984
|
-
month: "2026-03".to_string(),
|
|
985
|
-
commands: 5,
|
|
986
|
-
input_tokens: 100,
|
|
987
|
-
output_tokens: 50,
|
|
988
|
-
saved_tokens: 1000,
|
|
989
|
-
savings_pct: 40.0,
|
|
990
|
-
total_time_ms: 0,
|
|
991
|
-
avg_time_ms: 0,
|
|
992
|
-
},
|
|
993
|
-
MonthStats {
|
|
994
|
-
month: "2026-01".to_string(),
|
|
995
|
-
commands: 10,
|
|
996
|
-
input_tokens: 200,
|
|
997
|
-
output_tokens: 100,
|
|
998
|
-
saved_tokens: 2000,
|
|
999
|
-
savings_pct: 60.0,
|
|
1000
|
-
total_time_ms: 0,
|
|
1001
|
-
avg_time_ms: 0,
|
|
1002
|
-
},
|
|
1003
|
-
];
|
|
1004
|
-
|
|
1005
|
-
let merged = merge_monthly(None, rtk);
|
|
1006
|
-
assert_eq!(merged.len(), 2);
|
|
1007
|
-
assert_eq!(merged[0].label, "2026-01");
|
|
1008
|
-
assert_eq!(merged[1].label, "2026-03");
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
#[test]
|
|
1012
|
-
fn test_compute_weighted_input_cpt() {
|
|
1013
|
-
let mut p = PeriodEconomics::new("2026-01");
|
|
1014
|
-
p.cc_cost = Some(100.0);
|
|
1015
|
-
p.cc_input_tokens = Some(1000);
|
|
1016
|
-
p.cc_output_tokens = Some(500);
|
|
1017
|
-
p.cc_cache_create_tokens = Some(200);
|
|
1018
|
-
p.cc_cache_read_tokens = Some(5000);
|
|
1019
|
-
p.rtk_saved_tokens = Some(10_000);
|
|
1020
|
-
|
|
1021
|
-
p.compute_weighted_metrics();
|
|
1022
|
-
|
|
1023
|
-
// weighted_units = 1000 + 5*500 + 1.25*200 + 0.1*5000 = 1000 + 2500 + 250 + 500 = 4250
|
|
1024
|
-
// input_cpt = 100 / 4250 = 0.0235294...
|
|
1025
|
-
// savings = 10000 * 0.0235294... = 235.29...
|
|
1026
|
-
|
|
1027
|
-
assert!(p.weighted_input_cpt.is_some());
|
|
1028
|
-
let cpt = p.weighted_input_cpt.unwrap();
|
|
1029
|
-
assert!((cpt - (100.0 / 4250.0)).abs() < 1e-6);
|
|
1030
|
-
|
|
1031
|
-
assert!(p.savings_weighted.is_some());
|
|
1032
|
-
let savings = p.savings_weighted.unwrap();
|
|
1033
|
-
assert!((savings - 235.294).abs() < 0.01);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
#[test]
|
|
1037
|
-
fn test_compute_weighted_metrics_zero_tokens() {
|
|
1038
|
-
let mut p = PeriodEconomics::new("2026-01");
|
|
1039
|
-
p.cc_cost = Some(100.0);
|
|
1040
|
-
p.cc_input_tokens = Some(0);
|
|
1041
|
-
p.cc_output_tokens = Some(0);
|
|
1042
|
-
p.cc_cache_create_tokens = Some(0);
|
|
1043
|
-
p.cc_cache_read_tokens = Some(0);
|
|
1044
|
-
p.rtk_saved_tokens = Some(5000);
|
|
1045
|
-
|
|
1046
|
-
p.compute_weighted_metrics();
|
|
1047
|
-
|
|
1048
|
-
assert!(p.weighted_input_cpt.is_none());
|
|
1049
|
-
assert!(p.savings_weighted.is_none());
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
#[test]
|
|
1053
|
-
fn test_compute_weighted_metrics_no_cache() {
|
|
1054
|
-
let mut p = PeriodEconomics::new("2026-01");
|
|
1055
|
-
p.cc_cost = Some(60.0);
|
|
1056
|
-
p.cc_input_tokens = Some(1000);
|
|
1057
|
-
p.cc_output_tokens = Some(1000);
|
|
1058
|
-
p.cc_cache_create_tokens = Some(0);
|
|
1059
|
-
p.cc_cache_read_tokens = Some(0);
|
|
1060
|
-
p.rtk_saved_tokens = Some(3000);
|
|
1061
|
-
|
|
1062
|
-
p.compute_weighted_metrics();
|
|
1063
|
-
|
|
1064
|
-
// weighted_units = 1000 + 5*1000 = 6000
|
|
1065
|
-
// input_cpt = 60 / 6000 = 0.01
|
|
1066
|
-
// savings = 3000 * 0.01 = 30
|
|
1067
|
-
|
|
1068
|
-
assert!(p.weighted_input_cpt.is_some());
|
|
1069
|
-
let cpt = p.weighted_input_cpt.unwrap();
|
|
1070
|
-
assert!((cpt - 0.01).abs() < 1e-6);
|
|
1071
|
-
|
|
1072
|
-
assert!(p.savings_weighted.is_some());
|
|
1073
|
-
let savings = p.savings_weighted.unwrap();
|
|
1074
|
-
assert!((savings - 30.0).abs() < 0.01);
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
#[test]
|
|
1078
|
-
fn test_set_ccusage_stores_per_type_tokens() {
|
|
1079
|
-
let mut p = PeriodEconomics::new("2026-01");
|
|
1080
|
-
let metrics = ccusage::CcusageMetrics {
|
|
1081
|
-
input_tokens: 1000,
|
|
1082
|
-
output_tokens: 500,
|
|
1083
|
-
cache_creation_tokens: 200,
|
|
1084
|
-
cache_read_tokens: 3000,
|
|
1085
|
-
total_tokens: 4700,
|
|
1086
|
-
total_cost: 50.0,
|
|
1087
|
-
};
|
|
1088
|
-
|
|
1089
|
-
p.set_ccusage(&metrics);
|
|
1090
|
-
|
|
1091
|
-
assert_eq!(p.cc_input_tokens, Some(1000));
|
|
1092
|
-
assert_eq!(p.cc_output_tokens, Some(500));
|
|
1093
|
-
assert_eq!(p.cc_cache_create_tokens, Some(200));
|
|
1094
|
-
assert_eq!(p.cc_cache_read_tokens, Some(3000));
|
|
1095
|
-
assert_eq!(p.cc_total_tokens, Some(4700));
|
|
1096
|
-
assert_eq!(p.cc_cost, Some(50.0));
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
#[test]
|
|
1100
|
-
fn test_compute_totals() {
|
|
1101
|
-
let periods = vec![
|
|
1102
|
-
PeriodEconomics {
|
|
1103
|
-
label: "2026-01".to_string(),
|
|
1104
|
-
cc_cost: Some(100.0),
|
|
1105
|
-
cc_total_tokens: Some(1_000_000),
|
|
1106
|
-
cc_active_tokens: Some(10_000),
|
|
1107
|
-
cc_input_tokens: Some(5000),
|
|
1108
|
-
cc_output_tokens: Some(5000),
|
|
1109
|
-
cc_cache_create_tokens: Some(100),
|
|
1110
|
-
cc_cache_read_tokens: Some(984_900),
|
|
1111
|
-
rtk_commands: Some(5),
|
|
1112
|
-
rtk_saved_tokens: Some(2000),
|
|
1113
|
-
rtk_savings_pct: Some(50.0),
|
|
1114
|
-
weighted_input_cpt: None,
|
|
1115
|
-
savings_weighted: None,
|
|
1116
|
-
blended_cpt: None,
|
|
1117
|
-
active_cpt: None,
|
|
1118
|
-
savings_blended: None,
|
|
1119
|
-
savings_active: None,
|
|
1120
|
-
},
|
|
1121
|
-
PeriodEconomics {
|
|
1122
|
-
label: "2026-02".to_string(),
|
|
1123
|
-
cc_cost: Some(200.0),
|
|
1124
|
-
cc_total_tokens: Some(2_000_000),
|
|
1125
|
-
cc_active_tokens: Some(20_000),
|
|
1126
|
-
cc_input_tokens: Some(10_000),
|
|
1127
|
-
cc_output_tokens: Some(10_000),
|
|
1128
|
-
cc_cache_create_tokens: Some(200),
|
|
1129
|
-
cc_cache_read_tokens: Some(1_979_800),
|
|
1130
|
-
rtk_commands: Some(10),
|
|
1131
|
-
rtk_saved_tokens: Some(3000),
|
|
1132
|
-
rtk_savings_pct: Some(60.0),
|
|
1133
|
-
weighted_input_cpt: None,
|
|
1134
|
-
savings_weighted: None,
|
|
1135
|
-
blended_cpt: None,
|
|
1136
|
-
active_cpt: None,
|
|
1137
|
-
savings_blended: None,
|
|
1138
|
-
savings_active: None,
|
|
1139
|
-
},
|
|
1140
|
-
];
|
|
1141
|
-
|
|
1142
|
-
let totals = compute_totals(&periods);
|
|
1143
|
-
assert_eq!(totals.cc_cost, 300.0);
|
|
1144
|
-
assert_eq!(totals.cc_total_tokens, 3_000_000);
|
|
1145
|
-
assert_eq!(totals.cc_active_tokens, 30_000);
|
|
1146
|
-
assert_eq!(totals.cc_input_tokens, 15_000);
|
|
1147
|
-
assert_eq!(totals.cc_output_tokens, 15_000);
|
|
1148
|
-
assert_eq!(totals.rtk_commands, 15);
|
|
1149
|
-
assert_eq!(totals.rtk_saved_tokens, 5000);
|
|
1150
|
-
assert_eq!(totals.rtk_avg_savings_pct, 55.0);
|
|
1151
|
-
|
|
1152
|
-
assert!(totals.weighted_input_cpt.is_some());
|
|
1153
|
-
assert!(totals.savings_weighted.is_some());
|
|
1154
|
-
assert!(totals.blended_cpt.is_some());
|
|
1155
|
-
assert!(totals.active_cpt.is_some());
|
|
1156
|
-
}
|
|
1157
|
-
}
|