codex-usage-tracking 0.3.0__py3-none-any.whl

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.
Files changed (50) hide show
  1. codex_usage_tracker/__init__.py +7 -0
  2. codex_usage_tracker/__main__.py +6 -0
  3. codex_usage_tracker/allowance.py +759 -0
  4. codex_usage_tracker/api_payloads.py +90 -0
  5. codex_usage_tracker/cli.py +1326 -0
  6. codex_usage_tracker/context.py +410 -0
  7. codex_usage_tracker/costing.py +176 -0
  8. codex_usage_tracker/dashboard.py +389 -0
  9. codex_usage_tracker/diagnostics.py +624 -0
  10. codex_usage_tracker/formatting.py +225 -0
  11. codex_usage_tracker/json_contracts.py +350 -0
  12. codex_usage_tracker/mcp_server.py +371 -0
  13. codex_usage_tracker/models.py +92 -0
  14. codex_usage_tracker/parser.py +491 -0
  15. codex_usage_tracker/paths.py +18 -0
  16. codex_usage_tracker/plugin_data/__init__.py +1 -0
  17. codex_usage_tracker/plugin_data/assets/icon.svg +8 -0
  18. codex_usage_tracker/plugin_data/dashboard/dashboard.css +954 -0
  19. codex_usage_tracker/plugin_data/dashboard/dashboard.js +1833 -0
  20. codex_usage_tracker/plugin_data/dashboard/dashboard_data.js +155 -0
  21. codex_usage_tracker/plugin_data/dashboard/dashboard_format.js +132 -0
  22. codex_usage_tracker/plugin_data/dashboard/dashboard_state.js +157 -0
  23. codex_usage_tracker/plugin_data/dashboard/dashboard_template.html +141 -0
  24. codex_usage_tracker/plugin_data/docs/assets/dashboard-calls.png +0 -0
  25. codex_usage_tracker/plugin_data/docs/assets/dashboard-details.png +0 -0
  26. codex_usage_tracker/plugin_data/docs/assets/dashboard-insights.png +0 -0
  27. codex_usage_tracker/plugin_data/docs/assets/dashboard-threads.png +0 -0
  28. codex_usage_tracker/plugin_data/docs/dashboard-guide.html +136 -0
  29. codex_usage_tracker/plugin_data/rate_cards/codex-credit-rates.json +69 -0
  30. codex_usage_tracker/plugin_data/skills/codex-usage-api/SKILL.md +62 -0
  31. codex_usage_tracker/plugin_data/skills/codex-usage-tracker/SKILL.md +47 -0
  32. codex_usage_tracker/plugin_installer.py +312 -0
  33. codex_usage_tracker/pricing.py +57 -0
  34. codex_usage_tracker/pricing_config.py +223 -0
  35. codex_usage_tracker/pricing_estimates.py +44 -0
  36. codex_usage_tracker/pricing_openai.py +253 -0
  37. codex_usage_tracker/projects.py +347 -0
  38. codex_usage_tracker/recommendations.py +270 -0
  39. codex_usage_tracker/reports.py +637 -0
  40. codex_usage_tracker/schema.py +71 -0
  41. codex_usage_tracker/server.py +400 -0
  42. codex_usage_tracker/store.py +666 -0
  43. codex_usage_tracker/support.py +147 -0
  44. codex_usage_tracker/threads.py +183 -0
  45. codex_usage_tracking-0.3.0.dist-info/METADATA +278 -0
  46. codex_usage_tracking-0.3.0.dist-info/RECORD +50 -0
  47. codex_usage_tracking-0.3.0.dist-info/WHEEL +5 -0
  48. codex_usage_tracking-0.3.0.dist-info/entry_points.txt +2 -0
  49. codex_usage_tracking-0.3.0.dist-info/licenses/LICENSE +21 -0
  50. codex_usage_tracking-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,410 @@
1
+ """Lazy raw-context loading for one aggregate usage record."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from codex_usage_tracker.paths import DEFAULT_DB_PATH
11
+ from codex_usage_tracker.store import query_usage_record
12
+
13
+ DEFAULT_CONTEXT_CHARS = 20_000
14
+ DEFAULT_CONTEXT_ENTRIES = 80
15
+
16
+ _SECRET_PATTERNS: tuple[tuple[re.Pattern[str], str], ...] = (
17
+ (re.compile(r"sk-[A-Za-z0-9_-]{10,}"), "[REDACTED_OPENAI_KEY]"),
18
+ (re.compile(r"github" r"_pat_[A-Za-z0-9_]{20,}"), "[REDACTED_GITHUB_TOKEN]"),
19
+ (re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}"), "[REDACTED_GITHUB_TOKEN]"),
20
+ (re.compile(r"\bA(?:KI|SI)A[0-9A-Z]{16}\b"), "[REDACTED_AWS_ACCESS_KEY]"),
21
+ (
22
+ re.compile(r"(?i)\baws_secret_access_key\s*[:=]\s*(['\"]?)[A-Za-z0-9/+=]{30,}\1"),
23
+ "aws_secret_access_key=[REDACTED_AWS_SECRET]",
24
+ ),
25
+ (
26
+ re.compile(r"(?i)\bAuthorization\s*[:=]\s*Bearer\s+[A-Za-z0-9._~+/-]+=*"),
27
+ "Authorization: Bearer [REDACTED_BEARER_TOKEN]",
28
+ ),
29
+ (re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/-]+=*"), "Bearer [REDACTED_BEARER_TOKEN]"),
30
+ (
31
+ re.compile(r"\bxox(?:a|b|p|r|s)-[A-Za-z0-9-]{10,}\b"),
32
+ "[REDACTED_SLACK_TOKEN]",
33
+ ),
34
+ (re.compile(r"\bxapp-[A-Za-z0-9-]{10,}\b"), "[REDACTED_SLACK_TOKEN]"),
35
+ (
36
+ re.compile(r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"),
37
+ "[REDACTED_JWT]",
38
+ ),
39
+ (
40
+ re.compile(
41
+ r"-----BEGIN [A-Z ]*PRIVATE KEY-----.*?-----END [A-Z ]*PRIVATE KEY-----",
42
+ re.S,
43
+ ),
44
+ "[REDACTED_PRIVATE_KEY]",
45
+ ),
46
+ (
47
+ re.compile(
48
+ r"(?i)\b([A-Z0-9_ -]*(?:password|api[_-]?key|token|secret|credential|private[_-]?key)[A-Z0-9_ -]*)\s*[:=]\s*"
49
+ r"(['\"]?)[^'\"\s,;}]+\2"
50
+ ),
51
+ r"\1=[REDACTED_SECRET]",
52
+ ),
53
+ )
54
+
55
+ _OUTPUT_OMITTED = (
56
+ "Tool output omitted by default. Reload with include_tool_output=true to inspect "
57
+ "redacted, size-limited output."
58
+ )
59
+
60
+
61
+ def load_call_context(
62
+ record_id: str,
63
+ db_path: Path = DEFAULT_DB_PATH,
64
+ max_chars: int = DEFAULT_CONTEXT_CHARS,
65
+ max_entries: int = DEFAULT_CONTEXT_ENTRIES,
66
+ include_tool_output: bool = False,
67
+ ) -> dict[str, Any]:
68
+ """Load logged turn context for one model call from the source JSONL file.
69
+
70
+ This intentionally reads raw transcript-like data only on demand. The returned
71
+ context is not written back to SQLite or embedded in dashboard HTML.
72
+ """
73
+
74
+ row = query_usage_record(db_path=db_path, record_id=record_id)
75
+ if row is None:
76
+ raise ValueError(f"No usage record found for record_id: {record_id}")
77
+
78
+ source_file = Path(str(row.get("source_file") or ""))
79
+ if not source_file.exists():
80
+ raise FileNotFoundError(f"Source log not found: {source_file}")
81
+
82
+ line_number = _positive_int(row.get("line_number"))
83
+ if line_number is None:
84
+ raise ValueError(f"Usage record has no valid source line: {record_id}")
85
+
86
+ target_turn_id = _optional_str(row.get("turn_id"))
87
+ entries, omitted = _read_context_entries(
88
+ path=source_file,
89
+ token_line=line_number,
90
+ target_turn_id=target_turn_id,
91
+ max_chars=max(1_000, max_chars),
92
+ max_entries=max(1, max_entries),
93
+ include_tool_output=include_tool_output,
94
+ )
95
+ return {
96
+ "schema": "codex-usage-tracker-context-v1",
97
+ "loaded_on_demand": True,
98
+ "raw_context_persisted": False,
99
+ "include_tool_output": include_tool_output,
100
+ "record": {
101
+ "record_id": row.get("record_id"),
102
+ "session_id": row.get("session_id"),
103
+ "thread_name": row.get("thread_name"),
104
+ "turn_id": row.get("turn_id"),
105
+ "event_timestamp": row.get("event_timestamp"),
106
+ "model": row.get("model"),
107
+ "effort": row.get("effort"),
108
+ "parent_session_id": row.get("parent_session_id"),
109
+ "parent_thread_name": row.get("parent_thread_name"),
110
+ "total_tokens": row.get("total_tokens"),
111
+ "cumulative_total_tokens": row.get("cumulative_total_tokens"),
112
+ },
113
+ "source": {
114
+ "file": str(source_file),
115
+ "line_number": line_number,
116
+ },
117
+ "entries": entries,
118
+ "omitted": omitted,
119
+ }
120
+
121
+
122
+ def _read_context_entries(
123
+ path: Path,
124
+ token_line: int,
125
+ target_turn_id: str | None,
126
+ max_chars: int,
127
+ max_entries: int,
128
+ include_tool_output: bool,
129
+ ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
130
+ candidates: list[dict[str, Any]] = []
131
+ omitted_parse_errors = 0
132
+ current_turn_id: str | None = None
133
+ collecting = target_turn_id is None
134
+
135
+ with path.open(encoding="utf-8") as handle:
136
+ for line_number, line in enumerate(handle, 1):
137
+ if line_number > token_line:
138
+ break
139
+ try:
140
+ envelope = json.loads(line)
141
+ except json.JSONDecodeError:
142
+ omitted_parse_errors += 1
143
+ continue
144
+ if not isinstance(envelope, dict):
145
+ continue
146
+ entry_type = _optional_str(envelope.get("type")) or "unknown"
147
+ payload = envelope.get("payload") if isinstance(envelope.get("payload"), dict) else {}
148
+ timestamp = _optional_str(envelope.get("timestamp"))
149
+
150
+ if entry_type == "turn_context":
151
+ current_turn_id = _optional_str(payload.get("turn_id"))
152
+ collecting = target_turn_id is None or current_turn_id == target_turn_id
153
+ if collecting:
154
+ candidates = []
155
+ candidates.append(
156
+ _context_entry(
157
+ line_number,
158
+ timestamp,
159
+ entry_type,
160
+ "Turn context",
161
+ _summarize_turn_context(payload),
162
+ )
163
+ )
164
+ continue
165
+
166
+ if not collecting:
167
+ continue
168
+
169
+ summarized = _summarize_payload(
170
+ entry_type=entry_type,
171
+ payload=payload,
172
+ include_tool_output=include_tool_output,
173
+ )
174
+ if summarized is not None:
175
+ candidates.append(
176
+ _context_entry(
177
+ line_number,
178
+ timestamp,
179
+ entry_type,
180
+ summarized["label"],
181
+ summarized["text"],
182
+ )
183
+ )
184
+
185
+ if (
186
+ line_number >= token_line
187
+ and entry_type == "event_msg"
188
+ and payload.get("type") == "token_count"
189
+ ):
190
+ break
191
+
192
+ limited, omitted = _limit_entries(candidates, max_chars=max_chars, max_entries=max_entries)
193
+ omitted["parse_errors"] = omitted_parse_errors
194
+ omitted["target_turn_id"] = target_turn_id
195
+ return limited, omitted
196
+
197
+
198
+ def _summarize_payload(
199
+ entry_type: str,
200
+ payload: dict[str, Any],
201
+ include_tool_output: bool,
202
+ ) -> dict[str, str] | None:
203
+ if entry_type == "response_item":
204
+ return _summarize_response_item(payload, include_tool_output=include_tool_output)
205
+ if entry_type == "event_msg":
206
+ return _summarize_event_msg(payload, include_tool_output=include_tool_output)
207
+ if entry_type == "compacted":
208
+ message = _optional_str(payload.get("message")) or "Compaction event"
209
+ return {"label": "Compaction", "text": message}
210
+ return None
211
+
212
+
213
+ def _summarize_turn_context(payload: dict[str, Any]) -> str:
214
+ fields = [
215
+ ("turn_id", payload.get("turn_id")),
216
+ ("cwd", payload.get("cwd")),
217
+ ("model", payload.get("model")),
218
+ ("effort", payload.get("effort")),
219
+ ("current_date", payload.get("current_date")),
220
+ ("timezone", payload.get("timezone")),
221
+ ]
222
+ lines = [f"{key}: {value}" for key, value in fields if value not in (None, "")]
223
+ summary = _optional_str(payload.get("summary"))
224
+ if summary:
225
+ lines.append(f"summary: {summary}")
226
+ return "\n".join(lines) if lines else "Turn context"
227
+
228
+
229
+ def _summarize_response_item(
230
+ payload: dict[str, Any],
231
+ include_tool_output: bool,
232
+ ) -> dict[str, str] | None:
233
+ item_type = _optional_str(payload.get("type")) or "response_item"
234
+ role = _optional_str(payload.get("role"))
235
+ name = _optional_str(payload.get("name"))
236
+ label_bits = [item_type]
237
+ if role:
238
+ label_bits.append(role)
239
+ if name:
240
+ label_bits.append(name)
241
+ label = " / ".join(label_bits)
242
+
243
+ content_text = _content_text(payload.get("content"))
244
+ if content_text:
245
+ return {"label": label, "text": content_text}
246
+
247
+ if "arguments" in payload:
248
+ return {
249
+ "label": label,
250
+ "text": f"Tool call arguments:\n{_jsonish(payload.get('arguments'))}",
251
+ }
252
+
253
+ if "input" in payload:
254
+ return {
255
+ "label": label,
256
+ "text": f"Tool input:\n{_jsonish(payload.get('input'))}",
257
+ }
258
+
259
+ if "output" in payload:
260
+ output = _optional_str(payload.get("output")) or _jsonish(payload.get("output"))
261
+ return {
262
+ "label": label,
263
+ "text": output if include_tool_output else _OUTPUT_OMITTED,
264
+ }
265
+
266
+ summary = _content_text(payload.get("summary"))
267
+ if summary:
268
+ return {"label": label, "text": summary}
269
+
270
+ action = payload.get("action")
271
+ if isinstance(action, dict):
272
+ return {"label": label, "text": f"Action:\n{_jsonish(action)}"}
273
+
274
+ return None
275
+
276
+
277
+ def _summarize_event_msg(
278
+ payload: dict[str, Any],
279
+ include_tool_output: bool,
280
+ ) -> dict[str, str] | None:
281
+ event_type = _optional_str(payload.get("type")) or "event_msg"
282
+ if event_type == "token_count":
283
+ info = payload.get("info") if isinstance(payload.get("info"), dict) else {}
284
+ return {"label": "Token count", "text": _jsonish(_token_count_summary(info))}
285
+
286
+ if "message" in payload:
287
+ return {"label": event_type, "text": _optional_str(payload.get("message")) or ""}
288
+
289
+ output_fields = [field for field in ("stdout", "stderr", "result") if field in payload]
290
+ if output_fields:
291
+ if not include_tool_output:
292
+ return {"label": event_type, "text": _OUTPUT_OMITTED}
293
+ text = "\n".join(f"{field}:\n{_jsonish(payload.get(field))}" for field in output_fields)
294
+ return {"label": event_type, "text": text}
295
+
296
+ compact = {
297
+ key: payload.get(key)
298
+ for key in ("call_id", "turn_id", "phase", "status", "duration_ms")
299
+ if key in payload
300
+ }
301
+ return {"label": event_type, "text": _jsonish(compact)} if compact else None
302
+
303
+
304
+ def _token_count_summary(info: dict[str, Any]) -> dict[str, Any]:
305
+ return {
306
+ "last_token_usage": info.get("last_token_usage"),
307
+ "total_token_usage": info.get("total_token_usage"),
308
+ "model_context_window": info.get("model_context_window"),
309
+ }
310
+
311
+
312
+ def _context_entry(
313
+ line_number: int,
314
+ timestamp: str | None,
315
+ entry_type: str,
316
+ label: str,
317
+ text: str,
318
+ ) -> dict[str, Any]:
319
+ return {
320
+ "line_number": line_number,
321
+ "timestamp": timestamp,
322
+ "type": entry_type,
323
+ "label": label,
324
+ "text": _redact(text),
325
+ "truncated": False,
326
+ }
327
+
328
+
329
+ def _limit_entries(
330
+ entries: list[dict[str, Any]],
331
+ max_chars: int,
332
+ max_entries: int,
333
+ ) -> tuple[list[dict[str, Any]], dict[str, Any]]:
334
+ limited_reversed: list[dict[str, Any]] = []
335
+ remaining = max_chars
336
+ omitted_entries = 0
337
+ omitted_chars = 0
338
+ selected = entries[-max_entries:]
339
+
340
+ for entry in reversed(selected):
341
+ text = str(entry.get("text") or "")
342
+ if remaining <= 0:
343
+ omitted_entries += 1
344
+ omitted_chars += len(text)
345
+ continue
346
+ if len(text) > remaining:
347
+ entry = dict(entry)
348
+ entry["text"] = text[:remaining] + "\n[TRUNCATED]"
349
+ entry["truncated"] = True
350
+ omitted_chars += len(text) - remaining
351
+ remaining = 0
352
+ else:
353
+ remaining -= len(text)
354
+ limited_reversed.append(entry)
355
+
356
+ limited = list(reversed(limited_reversed))
357
+ return limited, {
358
+ "older_entries": max(0, len(entries) - max_entries),
359
+ "over_budget_entries": omitted_entries,
360
+ "over_budget_chars": omitted_chars,
361
+ "max_chars": max_chars,
362
+ "max_entries": max_entries,
363
+ "returned_entries": len(limited),
364
+ }
365
+
366
+
367
+ def _content_text(value: object) -> str:
368
+ if value is None:
369
+ return ""
370
+ if isinstance(value, str):
371
+ return value
372
+ if isinstance(value, list):
373
+ pieces: list[str] = []
374
+ for item in value:
375
+ if isinstance(item, str):
376
+ pieces.append(item)
377
+ elif isinstance(item, dict):
378
+ text = item.get("text") or item.get("content")
379
+ if isinstance(text, str):
380
+ pieces.append(text)
381
+ return "\n".join(piece for piece in pieces if piece)
382
+ return _jsonish(value)
383
+
384
+
385
+ def _jsonish(value: object) -> str:
386
+ if isinstance(value, str):
387
+ return value
388
+ try:
389
+ return json.dumps(value, ensure_ascii=True, indent=2, sort_keys=True)
390
+ except TypeError:
391
+ return str(value)
392
+
393
+
394
+ def _redact(text: str) -> str:
395
+ redacted = text
396
+ for pattern, replacement in _SECRET_PATTERNS:
397
+ redacted = pattern.sub(replacement, redacted)
398
+ return redacted
399
+
400
+
401
+ def _positive_int(value: object) -> int | None:
402
+ try:
403
+ number = int(value) # type: ignore[arg-type]
404
+ except (TypeError, ValueError):
405
+ return None
406
+ return number if number > 0 else None
407
+
408
+
409
+ def _optional_str(value: object) -> str | None:
410
+ return value if isinstance(value, str) and value else None
@@ -0,0 +1,176 @@
1
+ """Cost estimation and pricing coverage calculations for aggregate usage rows."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from codex_usage_tracker.paths import DEFAULT_PRICING_PATH
9
+ from codex_usage_tracker.pricing_config import PricingConfig, load_pricing_config
10
+
11
+
12
+ def summarize_pricing_coverage(
13
+ rows: list[dict[str, Any]],
14
+ pricing: PricingConfig | None = None,
15
+ *,
16
+ model_field: str = "group_key",
17
+ ) -> dict[str, Any]:
18
+ """Summarize which aggregate model rows have usable local pricing."""
19
+
20
+ config = pricing or load_pricing_config()
21
+ coverage_rows: list[dict[str, Any]] = []
22
+ totals = {
23
+ "model_count": 0,
24
+ "priced_model_count": 0,
25
+ "unpriced_model_count": 0,
26
+ "total_tokens": 0.0,
27
+ "priced_tokens": 0.0,
28
+ "unpriced_tokens": 0.0,
29
+ "estimated_cost_usd": 0.0,
30
+ }
31
+
32
+ for row in rows:
33
+ model = row.get(model_field)
34
+ priced_as = config.priced_as(model)
35
+ copy = dict(row)
36
+ copy["model"] = model
37
+ copy["priced"] = priced_as is not None
38
+ copy["priced_as"] = priced_as
39
+ copy["pricing_estimated"] = config.is_estimated_model(model)
40
+ copy["estimated_cost_usd"] = estimate_cost_usd(copy, config, model=model)
41
+ total_tokens = _number(copy.get("total_tokens"))
42
+ totals["model_count"] += 1
43
+ totals["total_tokens"] += total_tokens
44
+ if priced_as:
45
+ totals["priced_model_count"] += 1
46
+ totals["priced_tokens"] += total_tokens
47
+ else:
48
+ totals["unpriced_model_count"] += 1
49
+ totals["unpriced_tokens"] += total_tokens
50
+ if isinstance(copy["estimated_cost_usd"], int | float):
51
+ totals["estimated_cost_usd"] += float(copy["estimated_cost_usd"])
52
+ coverage_rows.append(copy)
53
+
54
+ total_tokens = totals["total_tokens"]
55
+ totals["priced_token_ratio"] = (
56
+ totals["priced_tokens"] / total_tokens if total_tokens else 0.0
57
+ )
58
+ coverage_rows.sort(
59
+ key=lambda row: (
60
+ 0 if row.get("priced") is False else 1,
61
+ -_number(row.get("total_tokens")),
62
+ )
63
+ )
64
+ return {
65
+ "schema": "codex-usage-tracker-pricing-coverage-v1",
66
+ **totals,
67
+ "pricing_loaded": config.loaded and not config.error,
68
+ "pricing_path": str(config.path),
69
+ "pricing_source": config.source,
70
+ "rows": coverage_rows,
71
+ }
72
+
73
+
74
+ def annotate_rows_with_efficiency(
75
+ rows: list[dict[str, Any]],
76
+ pricing: PricingConfig | None = None,
77
+ *,
78
+ model_field: str = "model",
79
+ pricing_path: Path = DEFAULT_PRICING_PATH,
80
+ ) -> list[dict[str, Any]]:
81
+ """Return copied rows with local cost estimates and efficiency flags."""
82
+
83
+ config = pricing or load_pricing_config(pricing_path)
84
+ annotated: list[dict[str, Any]] = []
85
+ for row in rows:
86
+ copy = dict(row)
87
+ model = copy.get(model_field)
88
+ cost = estimate_cost_usd(copy, config, model=model)
89
+ savings = estimate_cache_savings_usd(copy, config, model=model)
90
+ copy["estimated_cost_usd"] = cost
91
+ copy["estimated_cache_savings_usd"] = savings
92
+ copy["pricing_model"] = config.priced_as(model)
93
+ copy["pricing_estimated"] = config.is_estimated_model(model)
94
+ copy["efficiency_flags"] = efficiency_flags(copy)
95
+ annotated.append(copy)
96
+ return annotated
97
+
98
+
99
+ def estimate_cost_usd(
100
+ row: dict[str, Any], pricing: PricingConfig, *, model: object | None = None
101
+ ) -> float | None:
102
+ """Estimate call cost from aggregate tokens and local model rates."""
103
+
104
+ rates = pricing.rates_for(model if model is not None else row.get("model"))
105
+ if not rates:
106
+ return None
107
+
108
+ input_rate = rates.get("input_per_million")
109
+ cached_rate = rates.get("cached_input_per_million", input_rate)
110
+ output_rate = rates.get("output_per_million")
111
+ if input_rate is None or cached_rate is None or output_rate is None:
112
+ return None
113
+
114
+ cached_input = _number(row.get("cached_input_tokens"))
115
+ uncached_input = _number(row.get("uncached_input_tokens"))
116
+ if uncached_input <= 0:
117
+ uncached_input = max(_number(row.get("input_tokens")) - cached_input, 0.0)
118
+ output_tokens = _number(row.get("output_tokens"))
119
+
120
+ return (
121
+ (uncached_input * input_rate)
122
+ + (cached_input * cached_rate)
123
+ + (output_tokens * output_rate)
124
+ ) / 1_000_000
125
+
126
+
127
+ def estimate_cache_savings_usd(
128
+ row: dict[str, Any], pricing: PricingConfig, *, model: object | None = None
129
+ ) -> float | None:
130
+ """Estimate local cache savings when cached input has a lower configured rate."""
131
+
132
+ rates = pricing.rates_for(model if model is not None else row.get("model"))
133
+ if not rates:
134
+ return None
135
+ input_rate = rates.get("input_per_million")
136
+ cached_rate = rates.get("cached_input_per_million")
137
+ if input_rate is None or cached_rate is None or cached_rate >= input_rate:
138
+ return None
139
+ return (_number(row.get("cached_input_tokens")) * (input_rate - cached_rate)) / 1_000_000
140
+
141
+
142
+ def efficiency_flags(row: dict[str, Any]) -> list[str]:
143
+ """Generate aggregate-only signals worth reviewing."""
144
+
145
+ flags: list[str] = []
146
+ total_tokens = _number(row.get("total_tokens"))
147
+ output_tokens = _number(row.get("output_tokens"))
148
+ input_tokens = _number(row.get("input_tokens"))
149
+ context = _number(row.get("context_window_percent"))
150
+ cache = _number(row.get("cache_ratio"))
151
+ reasoning = _number(row.get("reasoning_output_ratio"))
152
+ cost = row.get("estimated_cost_usd")
153
+
154
+ if context >= 0.8:
155
+ flags.append("high context use")
156
+ elif context >= 0.5:
157
+ flags.append("elevated context use")
158
+ if reasoning >= 0.75 and output_tokens >= 100:
159
+ flags.append("high reasoning share")
160
+ if input_tokens >= 10_000 and cache < 0.1:
161
+ flags.append("low cache reuse")
162
+ if total_tokens >= 20_000 and output_tokens <= 100:
163
+ flags.append("expensive low-output call")
164
+ if isinstance(cost, int | float) and cost >= 1:
165
+ flags.append("high estimated cost")
166
+ return flags
167
+
168
+
169
+ def _number(value: object) -> float:
170
+ if isinstance(value, bool):
171
+ return float(int(value))
172
+ if isinstance(value, int | float):
173
+ return float(value)
174
+ if isinstance(value, str) and value.strip():
175
+ return float(value)
176
+ return 0.0