Skip to content

Commit 1b4f50b

Browse files
committed
fix(stats): track ImageGen / VideoGen costs in usage insights (#13)
Before this, only chat LLM calls flowed through `recordUsage()`. The ImageGen and VideoGen tools wrote to the content library but never reported to the global stats file, so: - `franklin insights` undercounted total 30-day spend by the exact amount spent on image + video generation. - The "Top models" breakdown never surfaced `openai/gpt-image-1`, `bytedance/seedance-2.0`, `xai/grok-imagine-video`, etc. even when they were the dominant cost in a session. - Monthly projections in insights were similarly low. On successful generation each tool now fires a fire-and-forget `recordUsage(model, 0, 0, costUsd, 0)`. Input/output tokens stay at 0 because image/video models don't bill by token; the cost is pulled from the live gateway catalog via `findModel` + `estimateCostUsd`, the same path the AskUser cost preview uses. When the catalog lookup fails the tools fall back to hardcoded estimates so stats still get an entry (under-approximate rather than missing). The stats write is intentionally fire-and-forget and error-swallowing: a `~/.blockrun/stats.json` write failure must not turn a paid generation into a user-visible error. Scope is deliberately small — only the two tools, +33 lines, zero schema or behavior change to the existing tracker / insights engine. Any record already written by chat flows continues to work unchanged.
1 parent f8570ce commit 1b4f50b

2 files changed

Lines changed: 33 additions & 0 deletions

File tree

src/tools/imagegen.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import type { ContentLibrary } from '../content/library.js';
2222
import { checkImageBudget, recordImageAsset } from '../content/record-image.js';
2323
import { ModelClient } from '../agent/llm.js';
2424
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
25+
import { recordUsage } from '../stats/tracker.js';
26+
import { findModel, estimateCostUsd } from '../gateway-models.js';
2527

2628
interface ImageGenInput {
2729
prompt: string;
@@ -360,6 +362,20 @@ function buildExecute(deps: ImageGenDeps) {
360362
const sizeKB = (fileSize / 1024).toFixed(1);
361363
const revisedPrompt = imageData.revised_prompt ? `\nRevised prompt: ${imageData.revised_prompt}` : '';
362364

365+
// Stats: record this generation so it shows up in `franklin insights`
366+
// alongside chat spend. Before this, media generations bypassed
367+
// recordUsage entirely (only LLM chat calls were tracked), so the
368+
// insights panel under-reported total spend and never surfaced
369+
// image-generation models in its "top models" list. Fire-and-forget —
370+
// stats write must not fail a user-visible generation.
371+
void (async () => {
372+
try {
373+
const m = await findModel(imageModel);
374+
const estCost = m ? estimateCostUsd(m, { quantity: 1 }) : 0;
375+
recordUsage(imageModel, 0, 0, estCost, 0);
376+
} catch { /* ignore stats errors */ }
377+
})();
378+
363379
let contentSummary = '';
364380
if (contentId && deps.library) {
365381
const rec = recordImageAsset(deps.library, {

src/tools/videogen.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { loadConfig } from '../commands/config.js';
3737
import type { ContentLibrary } from '../content/library.js';
3838
import { ModelClient } from '../agent/llm.js';
3939
import { analyzeMediaRequest, renderProposalForAskUser } from '../agent/media-router.js';
40+
import { recordUsage } from '../stats/tracker.js';
41+
import { findModel, estimateCostUsd } from '../gateway-models.js';
4042

4143
interface VideoGenInput {
4244
prompt: string;
@@ -351,6 +353,21 @@ function buildExecute(deps: VideoGenDeps) {
351353
const sizeMB = (fileSize / 1_048_576).toFixed(1);
352354
const dur = videoData.duration_seconds ?? duration;
353355

356+
// Stats: record this generation so it shows up in `franklin insights`
357+
// alongside chat spend. Before this, media generations bypassed
358+
// recordUsage entirely, so the insights panel under-reported total
359+
// spend and never surfaced video models in its "top models" list.
360+
// Prefer the live gateway price when the model is in the catalog;
361+
// fall back to the legacy $0.05/s estimate otherwise. Fire-and-
362+
// forget — stats write must not fail a user-visible generation.
363+
void (async () => {
364+
try {
365+
const m = await findModel(videoModel);
366+
const estCost = m ? estimateCostUsd(m, { duration_seconds: dur }) : estimateVideoCostUsd(dur);
367+
recordUsage(videoModel, 0, 0, estCost, 0);
368+
} catch { /* ignore stats errors */ }
369+
})();
370+
354371
let contentSummary = '';
355372
if (contentId && deps.library) {
356373
const rec = deps.library.addAsset(contentId, {

0 commit comments

Comments
 (0)