@archeagle087/costcrew 0.4.0 → 0.4.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/bin.js +12 -5
- package/package.json +8 -5
- package/.github/workflows/ci.yml +0 -21
- package/.github/workflows/npm-release.yml +0 -107
- package/.github/workflows/prices-monthly.yml +0 -58
- package/.github/workflows/release.yml +0 -45
- package/Cargo.lock +0 -980
- package/Cargo.toml +0 -22
- package/assets/advise.png +0 -0
- package/assets/board.png +0 -0
- package/assets/catalog.png +0 -0
- package/assets/demo.png +0 -0
- package/assets/help.png +0 -0
- package/assets/mascot.png +0 -0
- package/assets/prices.png +0 -0
- package/assets/scan.png +0 -0
- package/assets/spend.png +0 -0
- package/assets/ui.png +0 -0
- package/data/prices.json +0 -1
- package/dist/costcrew-linux-x86_64.tar.gz +0 -0
- package/dist/costcrew-macos-aarch64.tar.gz +0 -0
- package/dist/costcrew-macos-x86_64.tar.gz +0 -0
- package/dist/costcrew-windows-x86_64.zip +0 -0
- package/install.ps1 +0 -38
- package/install.sh +0 -56
- package/packaging/npm/bin.js +0 -43
- package/packaging/npm/package.json +0 -20
- package/packaging/winget/0.4.0/Arch087146.costcrew.installer.yaml +0 -15
- package/packaging/winget/0.4.0/Arch087146.costcrew.locale.en-US.yaml +0 -28
- package/packaging/winget/0.4.0/Arch087146.costcrew.yaml +0 -5
- package/packaging/winget/README.md +0 -31
- package/scripts/render_screen.py +0 -221
- package/src/advise.rs +0 -154
- package/src/args.rs +0 -239
- package/src/board.rs +0 -227
- package/src/catalog.rs +0 -136
- package/src/demo.rs +0 -79
- package/src/fetch.rs +0 -155
- package/src/main.rs +0 -371
- package/src/price.rs +0 -218
- package/src/prices.rs +0 -182
- package/src/report.rs +0 -194
- package/src/scan.rs +0 -593
- package/src/spend.rs +0 -452
- package/src/ui.rs +0 -367
- package/work/costcrew-darwin-arm64/costcrew +0 -0
- package/work/costcrew-darwin-arm64/package.json +0 -9
- package/work/costcrew-darwin-x64/costcrew +0 -0
- package/work/costcrew-darwin-x64/package.json +0 -9
- package/work/costcrew-linux-x64/costcrew +0 -0
- package/work/costcrew-linux-x64/package.json +0 -9
- package/work/costcrew-win32-x64/costcrew.exe +0 -0
- package/work/costcrew-win32-x64/package.json +0 -9
package/src/prices.rs
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
//! `costcrew prices` — browse the bundled price database.
|
|
2
|
-
//!
|
|
3
|
-
//! The database ships with the binary: a curated vendor table plus the full
|
|
4
|
-
//! embedded OpenRouter catalog (data/prices.json), optionally enriched by the
|
|
5
|
-
//! on-disk cache (`fetch-prices`) and custom overrides (`--prices`). No
|
|
6
|
-
//! network, no log scanning — it's just a database you can read.
|
|
7
|
-
|
|
8
|
-
use crate::price::PriceDB;
|
|
9
|
-
use crate::report::{BOLD, CYAN, DIM, GREEN, RESET, YELLOW};
|
|
10
|
-
|
|
11
|
-
pub fn print_prices(db: &PriceDB, query: Option<&str>, sort: &str, top: usize, json: bool) {
|
|
12
|
-
// filter + dedupe: the same model can appear in curated + embedded +
|
|
13
|
-
// cache + custom; the LATEST entry is authoritative (matches lookup order)
|
|
14
|
-
let mut latest: std::collections::HashMap<String, &crate::price::PriceEntry> =
|
|
15
|
-
std::collections::HashMap::new();
|
|
16
|
-
for e in db.entries() {
|
|
17
|
-
if let Some(q) = query {
|
|
18
|
-
if !e.prefix.to_lowercase().contains(&q.to_lowercase()) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
latest.insert(e.prefix.clone(), e);
|
|
23
|
-
}
|
|
24
|
-
let mut rows: Vec<&crate::price::PriceEntry> = latest.into_values().collect();
|
|
25
|
-
|
|
26
|
-
match sort {
|
|
27
|
-
"price" | "cost" => rows.sort_by(|a, b| {
|
|
28
|
-
let ka = a.price.input + a.price.output;
|
|
29
|
-
let kb = b.price.input + b.price.output;
|
|
30
|
-
kb.partial_cmp(&ka).unwrap_or(std::cmp::Ordering::Equal) // priciest first
|
|
31
|
-
}),
|
|
32
|
-
"cheap" => rows.sort_by(|a, b| {
|
|
33
|
-
let ka = a.price.input + a.price.output;
|
|
34
|
-
let kb = b.price.input + b.price.output;
|
|
35
|
-
ka.partial_cmp(&kb).unwrap_or(std::cmp::Ordering::Equal) // cheapest first
|
|
36
|
-
}),
|
|
37
|
-
_ => rows.sort_by(|a, b| a.prefix.cmp(&b.prefix)), // name
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if json {
|
|
41
|
-
use serde_json::json;
|
|
42
|
-
let arr: Vec<_> = rows
|
|
43
|
-
.iter()
|
|
44
|
-
.take(top.max(1))
|
|
45
|
-
.map(|e| {
|
|
46
|
-
json!({
|
|
47
|
-
"model": e.prefix,
|
|
48
|
-
"input_per_1m": e.price.input,
|
|
49
|
-
"output_per_1m": e.price.output,
|
|
50
|
-
"cache_read_per_1m": e.price.cache_read,
|
|
51
|
-
"cache_write_per_1m": e.price.cache_write,
|
|
52
|
-
"per_hour": if e.price.per_hour > 0.0 { serde_json::Value::from(e.price.per_hour) } else { serde_json::Value::Null },
|
|
53
|
-
"source": e.src,
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
.collect();
|
|
57
|
-
println!(
|
|
58
|
-
"{}",
|
|
59
|
-
json!({
|
|
60
|
-
"total_models": db.entries().len(),
|
|
61
|
-
"matches": rows.len(),
|
|
62
|
-
"rows": arr,
|
|
63
|
-
})
|
|
64
|
-
);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
println!();
|
|
69
|
-
println!(
|
|
70
|
-
" {BOLD}costcrew price database{RESET} {DIM}· bundled, {} models{RESET}",
|
|
71
|
-
db.entries().len()
|
|
72
|
-
);
|
|
73
|
-
println!(
|
|
74
|
-
" {DIM}{} curated table · {} embedded snapshot · {} live cache{} · {} custom{RESET}",
|
|
75
|
-
db.entries().iter().filter(|e| e.src == "curated").count(),
|
|
76
|
-
db.embedded_count,
|
|
77
|
-
db.cache_count,
|
|
78
|
-
if db.embedded_fetched_at_ms > 0 {
|
|
79
|
-
format!(" (snapshot {})", date_of(db.embedded_fetched_at_ms))
|
|
80
|
-
} else {
|
|
81
|
-
String::new()
|
|
82
|
-
},
|
|
83
|
-
db.custom_count,
|
|
84
|
-
);
|
|
85
|
-
println!(" {DIM}refresh anytime: {RESET}{CYAN}costcrew fetch-prices{RESET}");
|
|
86
|
-
println!();
|
|
87
|
-
if let Some(q) = query {
|
|
88
|
-
println!(" {BOLD}{} matches for \"{q}\"{RESET}{DIM} (--sort name|price|cheap, --top N, --json){RESET}", rows.len());
|
|
89
|
-
println!();
|
|
90
|
-
}
|
|
91
|
-
println!(
|
|
92
|
-
" {:<44} {:>9} {:>9} {:>10} {:<8}",
|
|
93
|
-
"model", "in $/1M", "out $/1M", "cache $/1M", "source"
|
|
94
|
-
);
|
|
95
|
-
println!(" {}", "-".repeat(44 + 3 + 9 + 3 + 9 + 3 + 10 + 2 + 8));
|
|
96
|
-
|
|
97
|
-
for e in rows.iter().take(top) {
|
|
98
|
-
let (i, o, c) = (e.price.input, e.price.output, e.price.cache_read);
|
|
99
|
-
let fmt = |v: f64| {
|
|
100
|
-
if v > 0.0 {
|
|
101
|
-
format!("{v:>9.3}")
|
|
102
|
-
} else {
|
|
103
|
-
" ?".to_string()
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
let src_col = match e.src.as_str() {
|
|
107
|
-
"curated" => GREEN,
|
|
108
|
-
"embedded" => CYAN,
|
|
109
|
-
"cache" => YELLOW,
|
|
110
|
-
_ => DIM,
|
|
111
|
-
};
|
|
112
|
-
println!(
|
|
113
|
-
" {:<44} {} {} {:>10} ({src_col}{:<6}{DIM}){RESET}",
|
|
114
|
-
truncate(&e.prefix, 44),
|
|
115
|
-
fmt(i),
|
|
116
|
-
fmt(o),
|
|
117
|
-
fmt(c),
|
|
118
|
-
e.src,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if rows.len() > top {
|
|
123
|
-
println!();
|
|
124
|
-
println!(
|
|
125
|
-
" {DIM}... {} more. narrower filter, or --top {}. e.g.:{RESET}",
|
|
126
|
-
rows.len() - top,
|
|
127
|
-
rows.len()
|
|
128
|
-
);
|
|
129
|
-
println!(
|
|
130
|
-
" {DIM} costcrew prices {}{RESET}",
|
|
131
|
-
query.unwrap_or("anthropic")
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
if rows.iter().take(top).any(|e| e.price.input == 0.0) {
|
|
135
|
-
println!();
|
|
136
|
-
println!(
|
|
137
|
-
" {DIM}? = free tier or unpriced. Unknown stays unknown — no invented prices.{RESET}"
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
println!();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
fn truncate(s: &str, n: usize) -> String {
|
|
144
|
-
if s.chars().count() <= n {
|
|
145
|
-
s.to_string()
|
|
146
|
-
} else {
|
|
147
|
-
s.chars().take(n - 1).collect::<String>() + "…"
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/// epoch ms → "Nov 10 2026" (civil-from-days, no chrono dependency)
|
|
152
|
-
fn date_of(ms: u64) -> String {
|
|
153
|
-
if ms == 0 {
|
|
154
|
-
return "undated".into();
|
|
155
|
-
}
|
|
156
|
-
let days = ms / 86_400_000;
|
|
157
|
-
// Howard Hinnant's civil_from_days
|
|
158
|
-
let z = days as i64 + 719_468;
|
|
159
|
-
let era = z.div_euclid(146_097);
|
|
160
|
-
let doe = z.rem_euclid(146_097);
|
|
161
|
-
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
|
162
|
-
let y = yoe + era * 400;
|
|
163
|
-
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
164
|
-
let mp = (5 * doy + 2) / 153;
|
|
165
|
-
let d = doy - (153 * mp + 2) / 5 + 1;
|
|
166
|
-
let m = if mp < 10 { mp + 3 } else { mp - 9 };
|
|
167
|
-
let y = if m <= 2 { y + 1 } else { y };
|
|
168
|
-
const MON: [&str; 12] = [
|
|
169
|
-
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
170
|
-
];
|
|
171
|
-
format!("{} {} {}", MON[(m - 1) as usize], d, y)
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
#[cfg(test)]
|
|
175
|
-
mod tests {
|
|
176
|
-
use super::date_of;
|
|
177
|
-
#[test]
|
|
178
|
-
fn epoch_to_civil() {
|
|
179
|
-
assert_eq!(date_of(1_794_274_886_057), "Nov 10 2026");
|
|
180
|
-
assert_eq!(date_of(0), "undated");
|
|
181
|
-
}
|
|
182
|
-
}
|
package/src/report.rs
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
//! Terminal table + shareable markdown report.
|
|
2
|
-
|
|
3
|
-
use crate::scan::Row;
|
|
4
|
-
|
|
5
|
-
pub const RESET: &str = "\x1b[0m";
|
|
6
|
-
pub const DIM: &str = "\x1b[2m";
|
|
7
|
-
pub const BOLD: &str = "\x1b[1m";
|
|
8
|
-
pub const CYAN: &str = "\x1b[36m";
|
|
9
|
-
pub const YELLOW: &str = "\x1b[33m";
|
|
10
|
-
pub const GREEN: &str = "\x1b[32m";
|
|
11
|
-
pub const RED: &str = "\x1b[31m";
|
|
12
|
-
|
|
13
|
-
pub struct Totals {
|
|
14
|
-
pub total_cost: f64,
|
|
15
|
-
pub total_requests: u64,
|
|
16
|
-
pub unknown_models: usize,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
pub fn print_table(rows: &[Row], since_days: u32, totals: &Totals) {
|
|
20
|
-
let period = if since_days == 1 {
|
|
21
|
-
"last 24h".to_string()
|
|
22
|
-
} else {
|
|
23
|
-
format!("last {since_days}d")
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
println!();
|
|
27
|
-
println!(" {BOLD}costcrew{RESET} {DIM}· AI agent spend · {period}{RESET}");
|
|
28
|
-
println!();
|
|
29
|
-
|
|
30
|
-
let widths = col_widths(rows);
|
|
31
|
-
let cost_w = 10;
|
|
32
|
-
|
|
33
|
-
// header
|
|
34
|
-
print!(" ");
|
|
35
|
-
for (i, h) in [
|
|
36
|
-
"provider",
|
|
37
|
-
"model",
|
|
38
|
-
"reqs",
|
|
39
|
-
"tokens in",
|
|
40
|
-
"tokens out",
|
|
41
|
-
"cache",
|
|
42
|
-
]
|
|
43
|
-
.iter()
|
|
44
|
-
.enumerate()
|
|
45
|
-
{
|
|
46
|
-
print!("{DIM}{:>width$}{RESET} ", h, width = widths[i]);
|
|
47
|
-
}
|
|
48
|
-
println!("{DIM}{:>width$}{RESET}", "cost (usd)", width = cost_w);
|
|
49
|
-
|
|
50
|
-
// separator
|
|
51
|
-
print!(" ");
|
|
52
|
-
for w in &widths {
|
|
53
|
-
print!("{DIM}{}{RESET} ", "-".repeat(*w));
|
|
54
|
-
}
|
|
55
|
-
println!("{DIM}{}{RESET}", "-".repeat(cost_w));
|
|
56
|
-
|
|
57
|
-
// rows
|
|
58
|
-
for r in rows {
|
|
59
|
-
print!(" ");
|
|
60
|
-
print!("{:>w$} ", r.provider, w = widths[0]);
|
|
61
|
-
print!("{:>w$} ", truncate(&r.model, widths[1]), w = widths[1]);
|
|
62
|
-
print!("{:>w$} ", r.requests, w = widths[2]);
|
|
63
|
-
print!("{:>w$} ", r.input_tokens, w = widths[3]);
|
|
64
|
-
print!("{:>w$} ", r.output_tokens, w = widths[4]);
|
|
65
|
-
print!("{:>w$} ", r.cache_tokens, w = widths[5]);
|
|
66
|
-
if r.known_price {
|
|
67
|
-
let color = cost_color(r.cost);
|
|
68
|
-
print!("{}{:>10.4}{}", color, r.cost, RESET);
|
|
69
|
-
} else {
|
|
70
|
-
print!("{:>10}", "?");
|
|
71
|
-
}
|
|
72
|
-
println!();
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// totals
|
|
76
|
-
let t_in: u64 = rows.iter().map(|r| r.input_tokens).sum();
|
|
77
|
-
let t_out: u64 = rows.iter().map(|r| r.output_tokens).sum();
|
|
78
|
-
let t_cache: u64 = rows.iter().map(|r| r.cache_tokens).sum();
|
|
79
|
-
print!(" ");
|
|
80
|
-
for w in &widths[..2] {
|
|
81
|
-
print!("{DIM}{}{RESET} ", "-".repeat(*w));
|
|
82
|
-
}
|
|
83
|
-
print!("{BOLD}total{RESET} ");
|
|
84
|
-
print!("{:>w$} ", "", w = widths[2] + 1);
|
|
85
|
-
print!("{:>w$} ", t_in, w = widths[3] + 1);
|
|
86
|
-
print!("{:>w$} ", t_out, w = widths[4] + 1);
|
|
87
|
-
print!("{:>w$} ", t_cache, w = widths[5] + 1);
|
|
88
|
-
// -0.0 guard: an all-unpriced table shouldn't render as negative money
|
|
89
|
-
let shown = if totals.total_cost.abs() < 0.000_1 {
|
|
90
|
-
0.0
|
|
91
|
-
} else {
|
|
92
|
-
totals.total_cost
|
|
93
|
-
};
|
|
94
|
-
println!("{BOLD}{GREEN}${:>8.2}{RESET}", shown);
|
|
95
|
-
println!();
|
|
96
|
-
|
|
97
|
-
// bar chart of spend share
|
|
98
|
-
let max = rows.iter().map(|r| r.cost).fold(0.0_f64, f64::max);
|
|
99
|
-
if max > 0.0 {
|
|
100
|
-
for r in rows.iter().filter(|r| r.known_price) {
|
|
101
|
-
let scale = ((r.cost / max) * 24.0).round() as usize;
|
|
102
|
-
let units = if r.cost > 0.0 { scale.max(1) } else { 0 };
|
|
103
|
-
let bar: String = std::iter::repeat_n('#', units).collect();
|
|
104
|
-
let color = cost_color(r.cost);
|
|
105
|
-
println!(
|
|
106
|
-
" {:>26} {color}{bar}{RESET} ${:.2}",
|
|
107
|
-
truncate(&r.model, 26),
|
|
108
|
-
r.cost
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
println!();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if totals.unknown_models > 0 {
|
|
115
|
-
println!(
|
|
116
|
-
" {DIM}{} model(s) not priced yet — saw them, didn't bill them.{RESET}",
|
|
117
|
-
totals.unknown_models
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
fn cost_color(cost: f64) -> &'static str {
|
|
123
|
-
if cost < 1.0 {
|
|
124
|
-
GREEN
|
|
125
|
-
} else if cost < 10.0 {
|
|
126
|
-
YELLOW
|
|
127
|
-
} else {
|
|
128
|
-
RED
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
fn truncate(s: &str, max: usize) -> String {
|
|
133
|
-
if s.len() <= max {
|
|
134
|
-
s.to_string()
|
|
135
|
-
} else {
|
|
136
|
-
format!("{}…", &s[..max.saturating_sub(1)])
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
fn col_widths(rows: &[Row]) -> [usize; 6] {
|
|
141
|
-
let mut w = [
|
|
142
|
-
"provider".len(),
|
|
143
|
-
"model".len(),
|
|
144
|
-
"reqs".len(),
|
|
145
|
-
"tokens in".len(),
|
|
146
|
-
"tokens out".len(),
|
|
147
|
-
"cache".len(),
|
|
148
|
-
];
|
|
149
|
-
for r in rows {
|
|
150
|
-
w[0] = w[0].max(r.provider.len());
|
|
151
|
-
w[1] = w[1].max(r.model.len().min(40));
|
|
152
|
-
w[2] = w[2].max(r.requests.to_string().len());
|
|
153
|
-
w[3] = w[3].max(r.input_tokens.to_string().len());
|
|
154
|
-
w[4] = w[4].max(r.output_tokens.to_string().len());
|
|
155
|
-
w[5] = w[5].max(r.cache_tokens.to_string().len());
|
|
156
|
-
}
|
|
157
|
-
w
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
pub fn print_markdown(rows: &[Row], since_days: u32, totals: &Totals) {
|
|
161
|
-
println!("# costcrew report");
|
|
162
|
-
println!();
|
|
163
|
-
println!(
|
|
164
|
-
"AI agent spend, last {since_days} day(s). Generated by [costcrew](https://github.com/Arch087146/costcrew)."
|
|
165
|
-
);
|
|
166
|
-
println!();
|
|
167
|
-
println!("| provider | model | reqs | tokens in | tokens out | cached | cost (USD) |");
|
|
168
|
-
println!("|---|---|---:|---:|---:|---:|---:|");
|
|
169
|
-
for r in rows {
|
|
170
|
-
let cost = if r.known_price {
|
|
171
|
-
format!("${:.4}", r.cost)
|
|
172
|
-
} else {
|
|
173
|
-
"?".to_string()
|
|
174
|
-
};
|
|
175
|
-
println!(
|
|
176
|
-
"| {} | `{}` | {} | {} | {} | {} | {} |",
|
|
177
|
-
r.provider, r.model, r.requests, r.input_tokens, r.output_tokens, r.cache_tokens, cost
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
let t_in: u64 = rows.iter().map(|r| r.input_tokens).sum();
|
|
181
|
-
let t_out: u64 = rows.iter().map(|r| r.output_tokens).sum();
|
|
182
|
-
let t_cache: u64 = rows.iter().map(|r| r.cache_tokens).sum();
|
|
183
|
-
println!(
|
|
184
|
-
"| **total** | | **{}** | **{}** | **{}** | **{}** | **${:.2}** |",
|
|
185
|
-
totals.total_requests, t_in, t_out, t_cache, totals.total_cost
|
|
186
|
-
);
|
|
187
|
-
if totals.unknown_models > 0 {
|
|
188
|
-
println!();
|
|
189
|
-
println!(
|
|
190
|
-
"> {} model(s) had no price entry and were excluded from totals.",
|
|
191
|
-
totals.unknown_models
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
|
-
}
|