1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// ── Session Economics Tracking ───────────────────────────────────────────────
use serde::Serialize;
// ── Per-turn context budget ledger ────────────────────────────────────────────
/// Token cost of a single tool result within a turn.
#[derive(Debug, Clone)]
pub struct ToolCost {
pub name: String,
/// Estimated tokens (result_chars / 4).
pub tokens: usize,
}
/// Per-turn breakdown of context consumed.
/// Populated at turn end and surfaced in the SPECULAR panel.
#[derive(Debug, Clone)]
pub struct TurnBudget {
/// Actual input tokens charged this turn (precise — from API usage delta).
pub input_tokens: usize,
/// Actual output tokens generated this turn (precise — from API usage delta).
pub output_tokens: usize,
/// Estimated prior-history tokens (chars / 4) — context already present before this turn.
pub history_est: usize,
/// Per-tool result costs (estimated tokens from result length).
pub tool_costs: Vec<ToolCost>,
/// Context window fill percentage at turn end.
pub context_pct: u8,
}
impl TurnBudget {
/// Compact ledger string for the SPECULAR panel and /budget command.
pub fn render(&self) -> String {
let total = self.input_tokens + self.output_tokens;
let mut parts = Vec::new();
if self.history_est > 0 {
parts.push(format!("prior hist ~{}t", self.history_est));
}
for tc in &self.tool_costs {
parts.push(format!("{} ~{}t", tc.name, tc.tokens));
}
if self.output_tokens > 0 {
parts.push(format!("model out {}t", self.output_tokens));
}
let breakdown = if parts.is_empty() {
String::new()
} else {
format!("\n {}", parts.join(" | "))
};
format!(
"Context budget: +{}t this turn ({}% ctx){}\n \
Tip: large tool results are the most common cause of context pressure.",
total, self.context_pct, breakdown
)
}
}
/// Tracks token usage and tool calls for a session.
pub struct SessionEconomics {
/// Input tokens accumulated across all calls.
pub input_tokens: usize,
/// Output tokens accumulated across all calls.
pub output_tokens: usize,
/// List of tool calls with name and success/fail status.
pub tools_used: Vec<ToolRecord>,
}
impl SessionEconomics {
/// Create a new empty economics tracker.
pub fn new() -> Self {
Self {
input_tokens: 0,
output_tokens: 0,
tools_used: Vec::new(),
}
}
/// Record a tool call.
pub fn record_tool(&mut self, name: &str, success: bool) {
self.tools_used.push(ToolRecord {
name: name.to_string(),
success,
});
}
}
impl Default for SessionEconomics {
fn default() -> Self {
Self {
input_tokens: 0,
output_tokens: 0,
tools_used: Vec::new(),
}
}
}
/// A record of a tool call.
#[derive(Serialize, Clone, Debug)]
pub struct ToolRecord {
pub name: String,
pub success: bool,
}
// ── Pricing constants ─────────────────────────────────────────────────────────
/// Input token price: $0.002 per 1K tokens.
pub const INPUT_PRICE_PER_1K: f64 = 0.002;
/// Output token price: $0.006 per 1K tokens.
pub const OUTPUT_PRICE_PER_1K: f64 = 0.006;
// ── Report generation ────────────────────────────────────────────────────────
impl SessionEconomics {
/// Calculate simulated cost based on token usage.
pub fn simulated_cost(&self) -> f64 {
let input_cost = (self.input_tokens as f64 / 1000.0) * INPUT_PRICE_PER_1K;
let output_cost = (self.output_tokens as f64 / 1000.0) * OUTPUT_PRICE_PER_1K;
input_cost + output_cost
}
/// Generate a JSON report of the session economics.
pub fn to_json(&self) -> String {
use serde_json::json;
json!({
"session_economics": {
"input_tokens": self.input_tokens,
"output_tokens": self.output_tokens,
"total_tokens": self.input_tokens + self.output_tokens,
"tools_used": self.tools_used.iter().map(|t| {
json!({
"name": t.name,
"success": t.success
})
}).collect::<Vec<_>>(),
"simulated_cost_usd": self.simulated_cost()
}
})
.to_string()
}
}