@swestash/agent-lens 0.10.0 → 0.11.0

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.
@@ -1242,6 +1242,15 @@ function classifyToolError(resultSummary) {
1242
1242
  }
1243
1243
 
1244
1244
  // ../core/dist/pricing.js
1245
+ var SYNTHETIC_MODEL = "<synthetic>";
1246
+ function unpricedModels(models) {
1247
+ const out = /* @__PURE__ */ new Set();
1248
+ for (const m of models) {
1249
+ if (m && m !== SYNTHETIC_MODEL && !rateForModel(m))
1250
+ out.add(m);
1251
+ }
1252
+ return [...out].sort();
1253
+ }
1245
1254
  var PRICE_TABLE = {
1246
1255
  // Opus 4.0 / 4.1 launched at $15/$75; Opus 4.5 cut the Opus tier to $5/$25 and 4.6/4.7/4.8 kept it.
1247
1256
  // The bare 'claude-opus-4' fallback stays at the legacy rate for 4.0/4.1; 4.5+ override below.
@@ -1250,17 +1259,65 @@ var PRICE_TABLE = {
1250
1259
  "claude-opus-4-6": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1251
1260
  "claude-opus-4-7": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1252
1261
  "claude-opus-4-8": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1262
+ "claude-opus-5": { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.5 },
1253
1263
  "claude-fable-5": { input: 10, output: 50, cacheWrite: 12.5, cacheRead: 1 },
1254
1264
  "claude-sonnet-4": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
1265
+ // Sonnet 5 list price. An introductory $2/$10 rate runs through 2026-08-31; we bill the list
1266
+ // price so the estimate doesn't silently change under a date, matching the "list price" label.
1267
+ "claude-sonnet-5": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 },
1255
1268
  "claude-haiku-4": { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.1 },
1256
1269
  "claude-3-5-haiku": { input: 0.8, output: 4, cacheWrite: 1, cacheRead: 0.08 },
1257
1270
  "claude-3-5-sonnet": { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.3 }
1258
1271
  };
1272
+ var CACHE_WRITE_MULTIPLIER = 1.25;
1273
+ var CACHE_READ_MULTIPLIER = 0.1;
1274
+ function toRate(v) {
1275
+ if (v === null || typeof v !== "object" || Array.isArray(v))
1276
+ return null;
1277
+ const o = v;
1278
+ const num = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : null;
1279
+ const input = num(o.input);
1280
+ const output = num(o.output);
1281
+ if (input === null || output === null)
1282
+ return null;
1283
+ if (o.cacheWrite !== void 0 && num(o.cacheWrite) === null)
1284
+ return null;
1285
+ if (o.cacheRead !== void 0 && num(o.cacheRead) === null)
1286
+ return null;
1287
+ return {
1288
+ input,
1289
+ output,
1290
+ cacheWrite: num(o.cacheWrite) ?? input * CACHE_WRITE_MULTIPLIER,
1291
+ cacheRead: num(o.cacheRead) ?? input * CACHE_READ_MULTIPLIER
1292
+ };
1293
+ }
1294
+ function mergePricing(base, overrides) {
1295
+ const table = { ...base };
1296
+ const invalid = [];
1297
+ const applied = [];
1298
+ if (overrides === null || typeof overrides !== "object" || Array.isArray(overrides)) {
1299
+ return { table, invalid, applied };
1300
+ }
1301
+ for (const [prefix, value] of Object.entries(overrides)) {
1302
+ const rate = toRate(value);
1303
+ if (!prefix.trim() || !rate)
1304
+ invalid.push(prefix);
1305
+ else {
1306
+ table[prefix] = rate;
1307
+ applied.push(prefix);
1308
+ }
1309
+ }
1310
+ return { table, invalid: invalid.sort(), applied: applied.sort() };
1311
+ }
1312
+ var activeTable = PRICE_TABLE;
1313
+ function usePricing(table) {
1314
+ activeTable = table;
1315
+ }
1259
1316
  function rateForModel(model) {
1260
1317
  if (!model)
1261
1318
  return null;
1262
1319
  let best = null;
1263
- for (const [prefix, rate] of Object.entries(PRICE_TABLE)) {
1320
+ for (const [prefix, rate] of Object.entries(activeTable)) {
1264
1321
  if (model.startsWith(prefix) && (!best || prefix.length > best.len)) {
1265
1322
  best = { len: prefix.length, rate };
1266
1323
  }
@@ -1383,6 +1440,18 @@ function resolveRetention() {
1383
1440
  const parsed = raw ? Number(raw) : NaN;
1384
1441
  return Number.isFinite(parsed) && parsed >= 0 ? { keepDays: parsed, origin: "env" } : { keepDays: DEFAULT_VERSIONS_KEEP_DAYS, origin: "default" };
1385
1442
  }
1443
+ function resolvePricing(cfg = readConfigFile()) {
1444
+ const { table, invalid, applied } = mergePricing(PRICE_TABLE, cfg?.pricing);
1445
+ return { table, origin: applied.length ? "file" : "default", applied, invalid };
1446
+ }
1447
+ function installPricing(cfg) {
1448
+ const resolved = cfg === void 0 ? resolvePricing() : resolvePricing(cfg);
1449
+ usePricing(resolved.table);
1450
+ if (resolved.invalid.length) {
1451
+ console.error(`agent-lens: ignoring ${resolved.invalid.length} malformed pricing override(s): ${resolved.invalid.join(", ")} (each entry needs numeric input + output; built-in rates kept)`);
1452
+ }
1453
+ return resolved;
1454
+ }
1386
1455
  function readConfigFile() {
1387
1456
  const repoRoot = findRepoRoot();
1388
1457
  const dataDir = resolveDataDir(repoRoot);
@@ -4235,10 +4304,16 @@ function printIngestReport(db, dbPath, r) {
4235
4304
  const usageRows = db.prepare("SELECT model, input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens FROM token_usage").all();
4236
4305
  let cost = 0;
4237
4306
  let totalTokens = 0;
4307
+ const unpriced2 = /* @__PURE__ */ new Map();
4238
4308
  for (const u of usageRows) {
4239
4309
  cost += costForUsage(u.model, u);
4240
4310
  totalTokens += u.input_tokens + u.output_tokens + u.cache_creation_input_tokens + u.cache_read_input_tokens;
4311
+ if (u.model && u.model !== SYNTHETIC_MODEL && !rateForModel(u.model)) {
4312
+ unpriced2.set(u.model, (unpriced2.get(u.model) ?? 0) + 1);
4313
+ }
4241
4314
  }
4315
+ const unpricedLine = unpriced2.size ? `
4316
+ unpriced=${unpriced2.size} model(s): ` + [...unpriced2].map(([m, n]) => `${m} (${n} records)`).join(", ") + ` \u2014 cost above excludes them; add rates under "pricing" in agent-lens.config.json` : "";
4242
4317
  const wfRuns = count("SELECT COUNT(*) n FROM workflow_results");
4243
4318
  const metaRows = count("SELECT COUNT(*) n FROM session_meta");
4244
4319
  const trRows = count("SELECT COUNT(*) n FROM tool_results");
@@ -4253,12 +4328,13 @@ function printIngestReport(db, dbPath, r) {
4253
4328
  tool_results=${trRows} (upserted=${r.trStats.upserted} skipped=${r.trStats.skipped} malformed=${r.trStats.malformed})
4254
4329
  findings=${r.detected.count} (${sevLine})
4255
4330
  file_changes=${r.fileChanges.count}
4256
- tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}`);
4331
+ tokens=${totalTokens.toLocaleString()} est_cost=$${cost.toFixed(2)} db=${dbPath}` + unpricedLine);
4257
4332
  }
4258
4333
  function runIngest(argv = process.argv.slice(2)) {
4259
4334
  const args = parseIngestArgs(argv);
4260
4335
  const archiveRoot = args.archive || resolveArchiveDir();
4261
4336
  const { path: dbPath } = resolveDbPath(args.db);
4337
+ installPricing();
4262
4338
  if (!existsSync5(archiveRoot)) {
4263
4339
  console.error(`agent-lens-ingest: archive not found: ${archiveRoot} (run 'agent-lens collect' first)`);
4264
4340
  process.exit(1);
@@ -4432,6 +4508,9 @@ function splitOf(rows) {
4432
4508
  function tokensOf(split) {
4433
4509
  return split.input + split.output + split.cache_creation + split.cache_read;
4434
4510
  }
4511
+ function unpricedOf(rows) {
4512
+ return unpricedModels(rows.filter((u) => u.i || u.o || u.cw || u.cr).map((u) => u.model));
4513
+ }
4435
4514
  function costOf(rows) {
4436
4515
  let cost = 0;
4437
4516
  for (const u of rows) {
@@ -4594,7 +4673,14 @@ function attachSessionCost(db, rows) {
4594
4673
  return rows.map(({ ai_title, slug, ...r }) => {
4595
4674
  const u = usageBySession.get(r.id) ?? [];
4596
4675
  const split = splitOf(u);
4597
- return { ...r, tokens: tokensOf(split), token_split: split, cost: costOf(u), title: ai_title || slug || null };
4676
+ return {
4677
+ ...r,
4678
+ tokens: tokensOf(split),
4679
+ token_split: split,
4680
+ cost: costOf(u),
4681
+ unpriced_models: unpricedOf(u),
4682
+ title: ai_title || slug || null
4683
+ };
4598
4684
  });
4599
4685
  }
4600
4686
  function loadToolCalls(db, id) {
@@ -4739,6 +4825,7 @@ function withSessionTotals(db, session, id, toolRows) {
4739
4825
  tokens: tokensOf(split),
4740
4826
  token_split: split,
4741
4827
  cost: costOf(usage),
4828
+ unpriced_models: unpricedOf(usage),
4742
4829
  title: session.ai_title || session.slug || null,
4743
4830
  tool_call_count: toolRows.length,
4744
4831
  tool_error_count,
@@ -5250,12 +5337,12 @@ function dashboardOverview(db, f) {
5250
5337
  GROUP BY t.model`, ...w.params);
5251
5338
  const tokens = zeroSplit();
5252
5339
  let cost = 0;
5253
- const unpriced = [];
5340
+ const unpriced2 = [];
5254
5341
  for (const u of usage) {
5255
5342
  addUsage(tokens, u);
5256
5343
  cost += costForUsage(u.model, usageForCost(u));
5257
5344
  if (u.model && !rateForModel(u.model) && (u.i || u.o || u.cw || u.cr))
5258
- unpriced.push(u.model);
5345
+ unpriced2.push(u.model);
5259
5346
  }
5260
5347
  const totalTokens = tokens.input + tokens.output + tokens.cache_creation + tokens.cache_read;
5261
5348
  const durs = queryAll(db, `SELECT tn.duration_ms d FROM turns tn JOIN sessions s ON s.id = tn.session_id
@@ -5275,7 +5362,7 @@ function dashboardOverview(db, f) {
5275
5362
  total_tokens: totalTokens,
5276
5363
  cache_read_ratio: totalTokens ? tokens.cache_read / totalTokens : 0,
5277
5364
  cost: Number(cost.toFixed(4)),
5278
- unpriced_models: [...new Set(unpriced)].sort(),
5365
+ unpriced_models: [...new Set(unpriced2)].sort(),
5279
5366
  turn_duration_ms: { p50: pct(durs, 50), p95: pct(durs, 95), count: durs.length },
5280
5367
  session_duration_ms: { p50: pct(sessDurs, 50), p95: pct(sessDurs, 95), count: sessDurs.length },
5281
5368
  workflows: workflowAgg(db, f)
@@ -5564,6 +5651,13 @@ function storage(db, dbPath) {
5564
5651
  last_ingested: row?.last ?? null
5565
5652
  };
5566
5653
  }
5654
+ function unpriced(db) {
5655
+ if (!tableExists(db, "token_usage"))
5656
+ return [];
5657
+ const rows = queryAll(db, `SELECT DISTINCT model FROM token_usage
5658
+ WHERE input_tokens + output_tokens + cache_creation_input_tokens + cache_read_input_tokens > 0`);
5659
+ return unpricedModels(rows.map((r) => r.model));
5660
+ }
5567
5661
  function engineVersion(db, table, sql, expected) {
5568
5662
  const in_data = tableExists(db, table) ? queryAll(db, sql).map((r) => r.v).filter((v) => Number.isFinite(v)) : [];
5569
5663
  return { expected, in_data, stale: in_data.some((v) => v < expected) };
@@ -5594,6 +5688,13 @@ function about(db, ctx) {
5594
5688
  },
5595
5689
  server: { host: ctx.host, port: ctx.port, loopback_only: ctx.loopbackOnly },
5596
5690
  retention: { versions_keep_days: retention.keepDays, origin: retention.origin },
5691
+ pricing: {
5692
+ origin: ctx.pricing.origin,
5693
+ models: Object.keys(ctx.pricing.table).length,
5694
+ applied: ctx.pricing.applied,
5695
+ invalid: ctx.pricing.invalid,
5696
+ unpriced: unpriced(db)
5697
+ },
5597
5698
  sources: loadSources().map((s) => ({ label: s.label, agent: s.agent, config_dir: s.configDir })),
5598
5699
  // storage() already carries last_ingested from the same MAX(ingested_at) it aggregates over —
5599
5700
  // reusing it avoids a second query AND keeps the tableExists guard that lastIngested() lacks.
@@ -5886,6 +5987,7 @@ async function startServer(overrides = {}) {
5886
5987
  const repoRoot = findRepoRoot(here);
5887
5988
  const { path: dbPath, origin: dbOrigin } = resolveDbPath(overrides.db);
5888
5989
  const { host, port } = resolveServerConfig({ port: overrides.port, host: overrides.host });
5990
+ const pricing = installPricing();
5889
5991
  const webDist = resolveWebDist(here, repoRoot);
5890
5992
  if (host !== "127.0.0.1" && host !== "localhost" && !process.env.AGENT_LENS_ALLOW_NONLOCAL) {
5891
5993
  console.error(`agent-lens-server: refusing to bind non-loopback host '${host}' (privacy). Set AGENT_LENS_ALLOW_NONLOCAL=1 to override.`);
@@ -5902,7 +6004,7 @@ async function startServer(overrides = {}) {
5902
6004
  webDist,
5903
6005
  triageDbPath,
5904
6006
  enforceLoopbackHost,
5905
- about: { db: { path: dbPath, origin: dbOrigin }, host, port, loopbackOnly: enforceLoopbackHost, repoRoot }
6007
+ about: { db: { path: dbPath, origin: dbOrigin }, host, port, loopbackOnly: enforceLoopbackHost, repoRoot, pricing }
5906
6008
  });
5907
6009
  try {
5908
6010
  await app.listen({ host, port });
@@ -6070,6 +6172,7 @@ cli.command("config", "Show the effective configuration (sources, paths, server)
6070
6172
  const archive = resolveArchiveDir(repoRoot);
6071
6173
  const triageDb = triageDbFor(dbPath);
6072
6174
  const { keepDays, origin: keepOrigin } = resolveRetention();
6175
+ const pricing = resolvePricing();
6073
6176
  console.log("agent-lens config (precedence: flag > env > config file > default)\n");
6074
6177
  console.log("Paths");
6075
6178
  console.log(` config file ${configFile ?? "(none \u2014 using built-in default source)"}`);
@@ -6083,6 +6186,10 @@ cli.command("config", "Show the effective configuration (sources, paths, server)
6083
6186
  console.log(` non-local bind ${process.env.AGENT_LENS_ALLOW_NONLOCAL ? "allowed (AGENT_LENS_ALLOW_NONLOCAL)" : "blocked (loopback only)"}`);
6084
6187
  console.log("\nRetention");
6085
6188
  console.log(` .versions keep ${keepDays} day(s) [${keepOrigin}]`);
6189
+ console.log("\nPricing");
6190
+ console.log(` models priced ${Object.keys(pricing.table).length} [${pricing.origin}]`);
6191
+ if (pricing.applied.length) console.log(` overrides ${pricing.applied.join(", ")}`);
6192
+ if (pricing.invalid.length) console.log(` ignored (bad) ${pricing.invalid.join(", ")}`);
6086
6193
  console.log("\nSources");
6087
6194
  for (const s of loadSources()) console.log(` ${s.label.padEnd(14)} ${s.configDir} (${s.agent})`);
6088
6195
  });