Skip to content

Commit a48cd99

Browse files
committed
Fix web export missing per-bubble token counts and cost metadata (closes #6)
- Extract tokenCount from each raw bubble (inputTokens, outputTokens, cachedTokens) and render them in the per-bubble metadata line as "In: X / Out: Y / Cached: Z" - Aggregate into total_input_tokens, total_output_tokens, total_cached_tokens in the exported frontmatter - Derive total_cost_usd from composer usageData.cost / estimatedCost - Always include workspace_name (URL-decoded display name) in frontmatter Made-with: Cursor
1 parent 522ce78 commit a48cd99

1 file changed

Lines changed: 33 additions & 0 deletions

File tree

api/export_api.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ def export_chats():
252252
ctx_token_limit = ctx_window.get("tokenLimit", 0)
253253
ctx_pct_remaining = ctx_window.get("percentageRemainingFloat") or ctx_window.get("percentageRemaining")
254254

255+
# Token counts (AI bubbles only)
256+
tc_dict = (b.get("tokenCount") or {}) if btype == "assistant" else {}
257+
in_tok = tc_dict.get("inputTokens") or 0
258+
out_tok = tc_dict.get("outputTokens") or 0
259+
cached_tok = tc_dict.get("cachedTokens") or 0
260+
255261
bubbles.append({
256262
"type": btype,
257263
"text": text,
@@ -263,6 +269,9 @@ def export_chats():
263269
"contextTokensUsed": ctx_tokens_used if ctx_tokens_used > 0 else None,
264270
"contextTokenLimit": ctx_token_limit if ctx_token_limit > 0 else None,
265271
"contextPctRemaining": round(ctx_pct_remaining, 1) if ctx_pct_remaining else None,
272+
"inputTokens": in_tok if in_tok > 0 else None,
273+
"outputTokens": out_tok if out_tok > 0 else None,
274+
"cachedTokens": cached_tok if cached_tok > 0 else None,
266275
})
267276

268277
bubbles.sort(key=lambda x: x["timestamp"] or 0)
@@ -287,6 +296,12 @@ def export_chats():
287296
files_removed = cd.get("removedFiles", 0)
288297
max_ctx_used = max((b_item.get("contextTokensUsed", 0) or 0) for b_item in bubbles) if bubbles else 0
289298
ctx_limit = max((b_item.get("contextTokenLimit", 0) or 0) for b_item in bubbles) if bubbles else 0
299+
total_input_tokens = sum(b_item.get("inputTokens") or 0 for b_item in bubbles)
300+
total_output_tokens = sum(b_item.get("outputTokens") or 0 for b_item in bubbles)
301+
total_cached_tokens = sum(b_item.get("cachedTokens") or 0 for b_item in bubbles)
302+
usage_data = cd.get("usageData") or {}
303+
total_cost_raw = usage_data.get("cost") or usage_data.get("estimatedCost")
304+
total_cost = total_cost_raw if isinstance(total_cost_raw, (int, float)) and total_cost_raw > 0 else None
290305

291306
# Build frontmatter
292307
created_ms = to_epoch_ms(cd.get("createdAt")) or ts_ms
@@ -296,9 +311,18 @@ def export_chats():
296311
md += f"created_at: {datetime.fromtimestamp(created_ms / 1000).isoformat()}\n"
297312
md += f"updated_at: {datetime.fromtimestamp(updated_at_ms / 1000).isoformat() if updated_at_ms else datetime.now().isoformat()}\n"
298313
md += f"workspace: {ws_slug}\n"
314+
md += f"workspace_name: {ws_display_name}\n"
299315
md += f"message_count: {len(bubbles)}\n"
300316
if model_name:
301317
md += f"model: {model_name}\n"
318+
if total_input_tokens:
319+
md += f"total_input_tokens: {total_input_tokens}\n"
320+
if total_output_tokens:
321+
md += f"total_output_tokens: {total_output_tokens}\n"
322+
if total_cached_tokens:
323+
md += f"total_cached_tokens: {total_cached_tokens}\n"
324+
if total_cost:
325+
md += f"total_cost_usd: {total_cost:.6f}\n"
302326
if total_response_ms:
303327
md += f"total_response_time_sec: {total_response_ms / 1000:.1f}\n"
304328
if total_thinking_ms:
@@ -326,6 +350,15 @@ def export_chats():
326350
meta_parts = []
327351
if bubble.get("model"):
328352
meta_parts.append(f"Model: {bubble['model']}")
353+
if bubble.get("inputTokens") or bubble.get("outputTokens"):
354+
tok_parts = []
355+
if bubble.get("inputTokens"):
356+
tok_parts.append(f"In: {bubble['inputTokens']:,}")
357+
if bubble.get("outputTokens"):
358+
tok_parts.append(f"Out: {bubble['outputTokens']:,}")
359+
if bubble.get("cachedTokens"):
360+
tok_parts.append(f"Cached: {bubble['cachedTokens']:,}")
361+
meta_parts.append(" / ".join(tok_parts))
329362
if bubble.get("responseTimeMs"):
330363
meta_parts.append(f"Response: {bubble['responseTimeMs'] / 1000:.1f}s")
331364
if bubble.get("thinkingDurationMs"):

0 commit comments

Comments
 (0)