Skip to content

Commit fe2003f

Browse files
committed
feat(json): emit modelBreakdowns in per-agent JSON reports
Per-agent subcommands (ccusage pi|opencode|amp|hermes|... daily/weekly/ monthly/session --json) compute per-model cost breakdowns — the table view renders them with --breakdown — but the shared per-agent JSON serializer never emitted them, forcing JSON consumers to re-derive model costs they cannot actually reconstruct. Add "modelBreakdowns" to agent_summary_json, mirroring the unified serializers (summary_json / session_summary_json). Purely additive: every pre-existing key and value is unchanged; the codex-native serializer (models object) is deliberately untouched. Tests: shared-shape insta snapshot now shows populated breakdowns for all four report kinds; pi daily JSON asserts a full single-element breakdown array with non-zero cost (the motivating case); fixture- driven copilot (real pricing via read_otel_file) and qwen (real JSONL fixture line) assertions pin their entire breakdown arrays.
1 parent cdda182 commit fe2003f

5 files changed

Lines changed: 94 additions & 0 deletions

File tree

‎rust/crates/ccusage/src/adapter/copilot/loader.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,17 @@ mod tests {
269269
assert_eq!(report["daily"][0]["outputTokens"], 50);
270270
assert_eq!(report["daily"][0]["totalTokens"], 175);
271271
assert_eq!(report["daily"][0]["totalCost"], 300.0);
272+
assert_eq!(
273+
report["daily"][0]["modelBreakdowns"],
274+
json!([{
275+
"modelName": "test-model",
276+
"inputTokens": 90,
277+
"outputTokens": 50,
278+
"cacheCreationTokens": 20,
279+
"cacheReadTokens": 10,
280+
"cost": 300.0
281+
}])
282+
);
272283
}
273284

274285
#[test]

‎rust/crates/ccusage/src/adapter/opencode/report.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub(crate) fn agent_summary_json(
4141
"totalTokens": row.total_tokens(),
4242
"totalCost": row.total_cost,
4343
"modelsUsed": row.models_used,
44+
"modelBreakdowns": row.model_breakdowns,
4445
});
4546
if let (Some(obj), Some(credits)) = (value.as_object_mut(), row.credits) {
4647
obj.insert("credits".to_string(), json!(credits));

‎rust/crates/ccusage/src/adapter/opencode/snapshots/ccusage__adapter__opencode__report__tests__snapshots_agent_summary_json_period_keys_and_session_metadata.snap‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ expression: "serde_json::json!({\n \"daily\": agent_summary_json(&daily, Agen
1010
"date": "2026-01-02",
1111
"inputTokens": 100,
1212
"messageCount": 3,
13+
"modelBreakdowns": [
14+
{
15+
"cacheCreationTokens": 10,
16+
"cacheReadTokens": 5,
17+
"cost": 0.25,
18+
"inputTokens": 100,
19+
"modelName": "gpt-5.2-codex",
20+
"outputTokens": 50
21+
}
22+
],
1323
"modelsUsed": [
1424
"gpt-5.2-codex",
1525
"claude-sonnet-4-20250514"
@@ -27,6 +37,16 @@ expression: "serde_json::json!({\n \"daily\": agent_summary_json(&daily, Agen
2737
"date": "2026-01-02",
2838
"inputTokens": 100,
2939
"messageCount": 3,
40+
"modelBreakdowns": [
41+
{
42+
"cacheCreationTokens": 10,
43+
"cacheReadTokens": 5,
44+
"cost": 0.25,
45+
"inputTokens": 100,
46+
"modelName": "gpt-5.2-codex",
47+
"outputTokens": 50
48+
}
49+
],
3050
"modelsUsed": [
3151
"gpt-5.2-codex",
3252
"claude-sonnet-4-20250514"
@@ -52,6 +72,16 @@ expression: "serde_json::json!({\n \"daily\": agent_summary_json(&daily, Agen
5272
"credits": 1.5,
5373
"inputTokens": 100,
5474
"messageCount": 3,
75+
"modelBreakdowns": [
76+
{
77+
"cacheCreationTokens": 10,
78+
"cacheReadTokens": 5,
79+
"cost": 0.25,
80+
"inputTokens": 100,
81+
"modelName": "gpt-5.2-codex",
82+
"outputTokens": 50
83+
}
84+
],
5585
"modelsUsed": [
5686
"gpt-5.2-codex",
5787
"claude-sonnet-4-20250514"
@@ -69,6 +99,16 @@ expression: "serde_json::json!({\n \"daily\": agent_summary_json(&daily, Agen
6999
"inputTokens": 100,
70100
"lastActivity": "2026-01-02T12:34:56.000Z",
71101
"messageCount": 3,
102+
"modelBreakdowns": [
103+
{
104+
"cacheCreationTokens": 10,
105+
"cacheReadTokens": 5,
106+
"cost": 0.25,
107+
"inputTokens": 100,
108+
"modelName": "gpt-5.2-codex",
109+
"outputTokens": 50
110+
}
111+
],
72112
"modelsUsed": [
73113
"gpt-5.2-codex",
74114
"claude-sonnet-4-20250514"
@@ -87,6 +127,16 @@ expression: "serde_json::json!({\n \"daily\": agent_summary_json(&daily, Agen
87127
"credits": 1.5,
88128
"inputTokens": 100,
89129
"messageCount": 3,
130+
"modelBreakdowns": [
131+
{
132+
"cacheCreationTokens": 10,
133+
"cacheReadTokens": 5,
134+
"cost": 0.25,
135+
"inputTokens": 100,
136+
"modelName": "gpt-5.2-codex",
137+
"outputTokens": 50
138+
}
139+
],
90140
"modelsUsed": [
91141
"gpt-5.2-codex",
92142
"claude-sonnet-4-20250514"
@@ -113,6 +163,16 @@ expression: "serde_json::json!({\n \"daily\": agent_summary_json(&daily, Agen
113163
"credits": 1.5,
114164
"inputTokens": 100,
115165
"messageCount": 3,
166+
"modelBreakdowns": [
167+
{
168+
"cacheCreationTokens": 10,
169+
"cacheReadTokens": 5,
170+
"cost": 0.25,
171+
"inputTokens": 100,
172+
"modelName": "gpt-5.2-codex",
173+
"outputTokens": 50
174+
}
175+
],
116176
"modelsUsed": [
117177
"gpt-5.2-codex",
118178
"claude-sonnet-4-20250514"

‎rust/crates/ccusage/src/adapter/qwen/mod.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,17 @@ mod tests {
126126
report["daily"][0]["modelsUsed"],
127127
json!(["qwen3-coder-plus"])
128128
);
129+
assert_eq!(
130+
report["daily"][0]["modelBreakdowns"],
131+
json!([{
132+
"modelName": "qwen3-coder-plus",
133+
"inputTokens": 100,
134+
"outputTokens": 50,
135+
"cacheCreationTokens": 0,
136+
"cacheReadTokens": 5,
137+
"cost": 0.0
138+
}])
139+
);
129140
}
130141

131142
#[test]

‎rust/crates/ccusage/src/main.rs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,17 @@ mod tests {
839839
assert_eq!(report["daily"][0]["totalTokens"], 180);
840840
assert_eq!(report["daily"][0]["totalCost"], json!(0.05));
841841
assert_eq!(report["daily"][0]["modelsUsed"], json!(["[pi] gpt-5.4"]));
842+
assert_eq!(
843+
report["daily"][0]["modelBreakdowns"],
844+
json!([{
845+
"modelName": "[pi] gpt-5.4",
846+
"inputTokens": 100,
847+
"outputTokens": 50,
848+
"cacheCreationTokens": 20,
849+
"cacheReadTokens": 10,
850+
"cost": 0.05
851+
}])
852+
);
842853
}
843854

844855
#[test]

0 commit comments

Comments
 (0)