@blockrun/franklin 3.15.60 → 3.15.62

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.
@@ -1054,6 +1054,15 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1054
1054
  // llm.ts if `tools` ended up empty, so it's safe to attach here.
1055
1055
  const callToolChoice = forceToolChoiceNextRound;
1056
1056
  forceToolChoiceNextRound = null;
1057
+ // Wall-clock start of the model call. Used by the recordUsage call
1058
+ // a few hundred lines below so franklin-stats.json captures real
1059
+ // latency. Verified 2026-05-05: `franklin stats` reported
1060
+ // `avgLat=0.0s` for every model across 5300+ requests because the
1061
+ // agent-loop callsite always passed 0 for latencyMs (proxy path
1062
+ // already measured correctly). `franklin insights` couldn't surface
1063
+ // "this model is consistently slow" or "fallback was faster" until
1064
+ // this was fixed.
1065
+ const llmCallStartedAt = Date.now();
1057
1066
  try {
1058
1067
  const result = await client.complete({
1059
1068
  model: resolvedModel,
@@ -1354,7 +1363,8 @@ export async function interactiveSession(config, getUserInput, onEvent, onAbortR
1354
1363
  // franklin-debug.log; `franklin insights` was therefore useless
1355
1364
  // for spotting a hot routing chain.
1356
1365
  const costEstimate = estimateCost(resolvedModel, inputTokens, usage.outputTokens, 1);
1357
- recordUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, 0, turnFailedModels.size > 0);
1366
+ const llmLatencyMs = Date.now() - llmCallStartedAt;
1367
+ recordUsage(resolvedModel, inputTokens, usage.outputTokens, costEstimate, llmLatencyMs, turnFailedModels.size > 0);
1358
1368
  // ── Circuit breakers: prevent infinite-loop wallet drain ──
1359
1369
  // Per-turn $-cap was removed in v3.11.0 — runaway loops are caught by
1360
1370
  // MAX_TOOL_CALLS_PER_TURN (25) and MAX_TINY_RESPONSES (2) above; the
@@ -230,6 +230,12 @@ function buildExecute(deps) {
230
230
  // minutes; text-to-image keeps the original 60s.
231
231
  const timeoutMs = referenceImage ? 180_000 : 60_000;
232
232
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
233
+ // Wall-clock start of the paid call, including 402 retry + (optional)
234
+ // 202 polling. Used by recordUsage below so franklin-stats.json
235
+ // populates avgLatencyMs for image models. Mirrors the agent-loop fix
236
+ // in 3.15.61 — same pattern, same reason: insights couldn't surface
237
+ // "Seedance is slower than grok" while every media call recorded 0.
238
+ const callStartedAt = Date.now();
233
239
  try {
234
240
  // First request — will get 402
235
241
  let response = await fetch(endpoint, {
@@ -362,11 +368,12 @@ function buildExecute(deps) {
362
368
  // insights panel under-reported total spend and never surfaced
363
369
  // image-generation models in its "top models" list. Fire-and-forget —
364
370
  // stats write must not fail a user-visible generation.
371
+ const latencyMs = Date.now() - callStartedAt;
365
372
  void (async () => {
366
373
  try {
367
374
  const m = await findModel(imageModel);
368
375
  const estCost = m ? estimateCostUsd(m, { quantity: 1 }) : 0;
369
- recordUsage(imageModel, 0, 0, estCost, 0);
376
+ recordUsage(imageModel, 0, 0, estCost, latencyMs);
370
377
  }
371
378
  catch { /* ignore stats errors */ }
372
379
  })();
@@ -325,7 +325,9 @@ export const modalCreateCapability = {
325
325
  body.cpu = cpuCoerced;
326
326
  if (typeof memoryCoerced === 'number')
327
327
  body.memory = memoryCoerced;
328
+ const callStartedAt = Date.now();
328
329
  const res = await postWithPayment(modalEndpoint('create'), body, 'Franklin Modal sandbox create', ctx.abortSignal, 90_000);
330
+ const latencyMs = Date.now() - callStartedAt;
329
331
  if (!res.ok) {
330
332
  const err = res.body.error ? String(res.body.error) : res.raw.slice(0, 300);
331
333
  // Surface the per-field validation issues — usually the
@@ -356,7 +358,7 @@ export const modalCreateCapability = {
356
358
  });
357
359
  // Stats — surface Modal usage in `franklin insights` like other paid tools.
358
360
  try {
359
- recordUsage(`modal/${tier}`, 0, 0, price, 0);
361
+ recordUsage(`modal/${tier}`, 0, 0, price, latencyMs);
360
362
  }
361
363
  catch { /* ignore */ }
362
364
  return {
@@ -448,7 +450,9 @@ export const modalExecCapability = {
448
450
  };
449
451
  if (coercedTimeout !== undefined)
450
452
  body.timeout = coercedTimeout;
453
+ const callStartedAt = Date.now();
451
454
  const res = await postWithPayment(modalEndpoint('exec'), body, 'Franklin Modal sandbox exec', ctx.abortSignal, Math.max(30_000, ((coercedTimeout ?? 300) + 30) * 1000));
455
+ const latencyMs = Date.now() - callStartedAt;
452
456
  if (!res.ok) {
453
457
  // 400 here usually means the agent built the wrong shape (bad
454
458
  // sandbox_id, malformed command). Dump the full raw body so the
@@ -479,7 +483,7 @@ export const modalExecCapability = {
479
483
  const hasAnyOutput = stdout.length > 0 || stderr.length > 0;
480
484
  const exitCode = rawExit !== null ? rawExit : (hasAnyOutput ? 0 : -1);
481
485
  try {
482
- recordUsage('modal/exec', 0, 0, EXEC_PRICE_USD, 0);
486
+ recordUsage('modal/exec', 0, 0, EXEC_PRICE_USD, latencyMs);
483
487
  }
484
488
  catch { /* ignore */ }
485
489
  const summary = `exit ${exitCode}` + (rawExit === null ? ' (inferred — no exit_code field in response)' : '');
@@ -527,13 +531,15 @@ export const modalStatusCapability = {
527
531
  }
528
532
  catch { /* ignore */ }
529
533
  try {
534
+ const callStartedAt = Date.now();
530
535
  const res = await postWithPayment(modalEndpoint('status'), { sandbox_id }, 'Franklin Modal sandbox status', ctx.abortSignal, 30_000);
536
+ const latencyMs = Date.now() - callStartedAt;
531
537
  if (!res.ok) {
532
538
  const err = res.body.error ? String(res.body.error) : res.raw.slice(0, 300);
533
539
  return { output: `ModalStatus failed (${res.status}): ${err}`, isError: true };
534
540
  }
535
541
  try {
536
- recordUsage('modal/status', 0, 0, STATUS_PRICE_USD, 0);
542
+ recordUsage('modal/status', 0, 0, STATUS_PRICE_USD, latencyMs);
537
543
  }
538
544
  catch { /* ignore */ }
539
545
  const status = res.body.status || 'unknown';
@@ -573,7 +579,9 @@ export const modalTerminateCapability = {
573
579
  }
574
580
  catch { /* ignore */ }
575
581
  try {
582
+ const callStartedAt = Date.now();
576
583
  const res = await postWithPayment(modalEndpoint('terminate'), { sandbox_id }, 'Franklin Modal sandbox terminate', ctx.abortSignal, 30_000);
584
+ const latencyMs = Date.now() - callStartedAt;
577
585
  // Always remove from tracker — even on failure, retrying is wasteful.
578
586
  sessionSandboxTracker.remove(sandbox_id);
579
587
  if (!res.ok) {
@@ -585,7 +593,7 @@ export const modalTerminateCapability = {
585
593
  };
586
594
  }
587
595
  try {
588
- recordUsage('modal/terminate', 0, 0, TERMINATE_PRICE_USD, 0);
596
+ recordUsage('modal/terminate', 0, 0, TERMINATE_PRICE_USD, latencyMs);
589
597
  }
590
598
  catch { /* ignore */ }
591
599
  return { output: `Sandbox \`${sandbox_id}\` terminated.` };
@@ -167,6 +167,12 @@ function buildExecute(deps) {
167
167
  'Content-Type': 'application/json',
168
168
  'User-Agent': `franklin/${VERSION}`,
169
169
  };
170
+ // Wall-clock start of the paid call (submit + poll + download). Fed
171
+ // to recordUsage below so franklin-stats.json populates avgLatencyMs
172
+ // for video models. Same fix as 3.15.61 (agent loop) — five
173
+ // recordUsage callsites in this codebase, three of them were
174
+ // hardcoding 0.
175
+ const callStartedAt = Date.now();
170
176
  const onAbort = (ctrl) => () => ctrl.abort();
171
177
  // Phase 1: submit the job. First POST triggers a 402; we sign and retry.
172
178
  // The signed paymentHeaders must be reused on every GET poll — the server
@@ -300,11 +306,12 @@ function buildExecute(deps) {
300
306
  // Prefer the live gateway price when the model is in the catalog;
301
307
  // fall back to the legacy $0.05/s estimate otherwise. Fire-and-
302
308
  // forget — stats write must not fail a user-visible generation.
309
+ const latencyMs = Date.now() - callStartedAt;
303
310
  void (async () => {
304
311
  try {
305
312
  const m = await findModel(videoModel);
306
313
  const estCost = m ? estimateCostUsd(m, { duration_seconds: dur }) : estimateVideoCostUsd(dur);
307
- recordUsage(videoModel, 0, 0, estCost, 0);
314
+ recordUsage(videoModel, 0, 0, estCost, latencyMs);
308
315
  }
309
316
  catch { /* ignore stats errors */ }
310
317
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.60",
3
+ "version": "3.15.62",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {