@blockrun/franklin 3.15.50 → 3.15.51

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 (2) hide show
  1. package/dist/agent/llm.js +58 -5
  2. package/package.json +1 -1
package/dist/agent/llm.js CHANGED
@@ -431,11 +431,64 @@ export class ModelClient {
431
431
  if (!response.ok) {
432
432
  const errorBody = await response.text().catch(() => 'unknown error');
433
433
  const message = extractApiErrorMessage(errorBody);
434
- yield {
435
- kind: 'error',
436
- payload: { status: response.status, message },
437
- };
438
- return;
434
+ // Runtime tool_choice retry. The static allowlist at line ~35
435
+ // catches the case where the request goes directly to a model
436
+ // whose name contains `deepseek-reasoner` / `openai/o1` /
437
+ // `openai/o3`. But the gateway sometimes ALIASES a different
438
+ // model name to a reasoner backend — verified 2026-05-04 in a
439
+ // live session: a request for `deepseek/deepseek-v4-pro`
440
+ // returned `400 Invalid request: 400 deepseek-reasoner does not
441
+ // support this tool_choice`, because the gateway routed v4-pro
442
+ // to a deepseek-reasoner upstream. The static allowlist can't
443
+ // know that. Catch the error, drop tool_choice, re-fire once.
444
+ // No payment re-sign needed — original 402 already settled, and
445
+ // the gateway treats this as the same logical request.
446
+ const lc = message.toLowerCase();
447
+ const looksLikeToolChoiceReject = response.status === 400 &&
448
+ lc.includes('tool_choice') &&
449
+ (lc.includes('not support') || lc.includes('unsupported') || lc.includes('does not support'));
450
+ if (looksLikeToolChoiceReject && requestPayload['tool_choice'] !== undefined) {
451
+ delete requestPayload['tool_choice'];
452
+ const retryBody = JSON.stringify(requestPayload);
453
+ if (this.debug) {
454
+ console.error(`[franklin] tool_choice rejected by upstream; retrying without it (model=${request.model})`);
455
+ }
456
+ response = await withAbortableTimeout(() => fetch(endpoint, {
457
+ method: 'POST',
458
+ headers,
459
+ body: retryBody,
460
+ signal: requestController.signal,
461
+ }), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
462
+ if (response.status === 402) {
463
+ const paymentHeader = await this.signPayment(response);
464
+ if (!paymentHeader) {
465
+ yield { kind: 'error', payload: { message: 'Payment signing failed' } };
466
+ return;
467
+ }
468
+ response = await withAbortableTimeout(() => fetch(endpoint, {
469
+ method: 'POST',
470
+ headers: { ...headers, ...paymentHeader },
471
+ body: retryBody,
472
+ signal: requestController.signal,
473
+ }), requestController, createModelTimeoutError('request', request.model, requestTimeoutMs), requestTimeoutMs);
474
+ }
475
+ if (!response.ok) {
476
+ const retryBodyText = await response.text().catch(() => 'unknown error');
477
+ yield {
478
+ kind: 'error',
479
+ payload: { status: response.status, message: extractApiErrorMessage(retryBodyText) },
480
+ };
481
+ return;
482
+ }
483
+ // Successful retry — fall through to SSE parsing below.
484
+ }
485
+ else {
486
+ yield {
487
+ kind: 'error',
488
+ payload: { status: response.status, message },
489
+ };
490
+ return;
491
+ }
439
492
  }
440
493
  // Parse SSE stream
441
494
  yield* this.parseSSEStream(response, requestController, streamTimeoutMs, request.model);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.15.50",
3
+ "version": "3.15.51",
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": {