@blockrun/franklin 3.8.42 → 3.8.44

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/proxy/server.js +156 -24
  2. package/package.json +1 -1
@@ -5,7 +5,7 @@ import os from 'node:os';
5
5
  import { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload, parsePaymentRequired, extractPaymentDetails, solanaKeyToBytes, SOLANA_NETWORK, } from '@blockrun/llm';
6
6
  import { recordUsage } from '../stats/tracker.js';
7
7
  import { appendAudit } from '../stats/audit.js';
8
- import { fetchWithFallback, buildFallbackChain, DEFAULT_FALLBACK_CONFIG, ROUTING_PROFILES, } from './fallback.js';
8
+ import { buildFallbackChain, DEFAULT_FALLBACK_CONFIG, ROUTING_PROFILES, } from './fallback.js';
9
9
  import { routeRequest, parseRoutingProfile, } from '../router/index.js';
10
10
  import { estimateCost } from '../pricing.js';
11
11
  import { VERSION } from '../config.js';
@@ -41,6 +41,57 @@ function log(...args) {
41
41
  catch { /* ignore */ }
42
42
  }
43
43
  const DEFAULT_MAX_TOKENS = 4096;
44
+ const DEFAULT_PROXY_REQUEST_TIMEOUT_MS = 45_000;
45
+ const DEFAULT_PROXY_STREAM_TIMEOUT_MS = 5 * 60 * 1000;
46
+ function parseTimeoutEnv(name, fallback) {
47
+ const raw = process.env[name];
48
+ if (!raw)
49
+ return fallback;
50
+ const parsed = Number.parseInt(raw, 10);
51
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
52
+ }
53
+ function getProxyRequestTimeoutMs() {
54
+ return parseTimeoutEnv('FRANKLIN_PROXY_REQUEST_TIMEOUT_MS', DEFAULT_PROXY_REQUEST_TIMEOUT_MS);
55
+ }
56
+ function getProxyStreamTimeoutMs() {
57
+ return parseTimeoutEnv('FRANKLIN_PROXY_STREAM_TIMEOUT_MS', DEFAULT_PROXY_STREAM_TIMEOUT_MS);
58
+ }
59
+ function createProxyTimeoutError(label, timeoutMs) {
60
+ return new Error(`${label} timed out after ${timeoutMs}ms`);
61
+ }
62
+ async function fetchWithTimeout(url, init, timeoutMs, label) {
63
+ if (timeoutMs <= 0)
64
+ return fetch(url, init);
65
+ const controller = new AbortController();
66
+ const timeoutError = createProxyTimeoutError(label, timeoutMs);
67
+ const timeout = setTimeout(() => {
68
+ try {
69
+ controller.abort(timeoutError);
70
+ }
71
+ catch { /* ignore */ }
72
+ }, timeoutMs);
73
+ try {
74
+ return await fetch(url, { ...init, signal: controller.signal });
75
+ }
76
+ catch (err) {
77
+ if (controller.signal.aborted)
78
+ throw timeoutError;
79
+ throw err;
80
+ }
81
+ finally {
82
+ clearTimeout(timeout);
83
+ }
84
+ }
85
+ function replaceModelInBody(body, model) {
86
+ try {
87
+ const parsed = JSON.parse(body);
88
+ parsed.model = model;
89
+ return JSON.stringify(parsed);
90
+ }
91
+ catch {
92
+ return body;
93
+ }
94
+ }
44
95
  // Per-model last output tokens for adaptive max_tokens (avoids cross-request pollution)
45
96
  const MAX_TRACKED_MODELS = 50;
46
97
  const lastOutputByModel = new Map();
@@ -62,10 +113,12 @@ const MODEL_SHORTCUTS = {
62
113
  // Anthropic
63
114
  sonnet: 'anthropic/claude-sonnet-4.6',
64
115
  claude: 'anthropic/claude-sonnet-4.6',
116
+ 'sonnet-4.6': 'anthropic/claude-sonnet-4.6',
65
117
  opus: 'anthropic/claude-opus-4.7',
66
118
  'opus-4.7': 'anthropic/claude-opus-4.7',
67
119
  'opus-4.6': 'anthropic/claude-opus-4.6',
68
- haiku: 'anthropic/claude-haiku-4.5',
120
+ haiku: 'anthropic/claude-haiku-4.5-20251001',
121
+ 'haiku-4.5': 'anthropic/claude-haiku-4.5-20251001',
69
122
  // OpenAI
70
123
  // `gpt` / `gpt5` / `gpt-5` follow the gateway's flagship — currently 5.5.
71
124
  gpt: 'openai/gpt-5.5',
@@ -87,12 +140,16 @@ const MODEL_SHORTCUTS = {
87
140
  o1: 'openai/o1',
88
141
  // Google
89
142
  gemini: 'google/gemini-2.5-pro',
143
+ 'gemini-2.5': 'google/gemini-2.5-pro',
90
144
  flash: 'google/gemini-2.5-flash',
91
145
  'gemini-3': 'google/gemini-3.1-pro',
146
+ 'gemini-3.1': 'google/gemini-3.1-pro',
92
147
  // xAI
93
148
  grok: 'xai/grok-3',
149
+ 'grok-3': 'xai/grok-3',
94
150
  'grok-4': 'xai/grok-4-0709',
95
151
  'grok-fast': 'xai/grok-4-1-fast-reasoning',
152
+ 'grok-4.1': 'xai/grok-4-1-fast-reasoning',
96
153
  // DeepSeek
97
154
  deepseek: 'deepseek/deepseek-chat',
98
155
  r1: 'deepseek/deepseek-reasoner',
@@ -111,9 +168,15 @@ const MODEL_SHORTCUTS = {
111
168
  devstral: 'nvidia/qwen3-coder-480b',
112
169
  // Minimax
113
170
  minimax: 'minimax/minimax-m2.7',
171
+ 'm2.7': 'minimax/minimax-m2.7',
114
172
  // Others
115
173
  glm: 'zai/glm-5.1',
174
+ 'glm-turbo': 'zai/glm-5-turbo',
175
+ 'glm5': 'zai/glm-5.1',
116
176
  kimi: 'moonshot/kimi-k2.6',
177
+ 'k2.6': 'moonshot/kimi-k2.6',
178
+ 'kimi-k2.5': 'moonshot/kimi-k2.5',
179
+ 'k2.5': 'moonshot/kimi-k2.5',
117
180
  };
118
181
  // Model pricing now uses shared source from src/pricing.ts
119
182
  function detectModelSwitch(parsed) {
@@ -369,13 +432,21 @@ export function createProxy(options) {
369
432
  };
370
433
  let response;
371
434
  let finalModel = requestModel;
435
+ const requestTimeoutMs = getProxyRequestTimeoutMs();
372
436
  // Use fallback chain if enabled
373
437
  if (fallbackEnabled && body && requestPath.includes('messages')) {
374
438
  const fallbackConfig = {
375
439
  ...DEFAULT_FALLBACK_CONFIG,
376
440
  chain: buildFallbackChain(requestModel),
377
441
  };
378
- const result = await fetchWithFallback(targetUrl, requestInit, body, fallbackConfig, (failedModel, status, nextModel) => {
442
+ const result = await fetchWithPaymentFallback(targetUrl, requestInit, body, fallbackConfig, {
443
+ method: req.method || 'POST',
444
+ headers,
445
+ chain,
446
+ baseWallet,
447
+ solanaWallet,
448
+ timeoutMs: requestTimeoutMs,
449
+ }, (failedModel, status, nextModel) => {
379
450
  log(`⚠️ ${failedModel} returned ${status}, falling back to ${nextModel}`);
380
451
  });
381
452
  response = result.response;
@@ -388,20 +459,14 @@ export function createProxy(options) {
388
459
  }
389
460
  }
390
461
  else {
391
- // Direct fetch without fallback (with timeout)
392
- const directCtrl = new AbortController();
393
- const directTimeout = setTimeout(() => directCtrl.abort(), 120_000); // 2min
394
- response = await fetch(targetUrl, { ...requestInit, signal: directCtrl.signal });
395
- clearTimeout(directTimeout);
396
- }
397
- // Handle 402 payment — body now has the correct model after fallback
398
- if (response.status === 402) {
399
- if (chain === 'solana' && solanaWallet) {
400
- response = await handleSolanaPayment(response, targetUrl, req.method || 'POST', headers, body, solanaWallet.privateKey, solanaWallet.address);
401
- }
402
- else if (baseWallet) {
403
- response = await handleBasePayment(response, targetUrl, req.method || 'POST', headers, body, baseWallet.privateKey, baseWallet.address);
404
- }
462
+ response = await fetchModelAttempt(targetUrl, requestInit, body, requestModel, {
463
+ method: req.method || 'POST',
464
+ headers,
465
+ chain,
466
+ baseWallet,
467
+ solanaWallet,
468
+ timeoutMs: requestTimeoutMs,
469
+ });
405
470
  }
406
471
  const responseHeaders = {};
407
472
  response.headers.forEach((v, k) => {
@@ -452,7 +517,7 @@ export function createProxy(options) {
452
517
  const decoder = new TextDecoder();
453
518
  let fullResponse = '';
454
519
  const STREAM_CAP = 5_000_000; // 5MB cap on accumulated stream
455
- const STREAM_TIMEOUT_MS = 5 * 60 * 1000; // 5 min timeout for entire stream
520
+ const STREAM_TIMEOUT_MS = getProxyStreamTimeoutMs();
456
521
  const streamDeadline = Date.now() + STREAM_TIMEOUT_MS;
457
522
  const pump = async () => {
458
523
  while (true) {
@@ -563,10 +628,77 @@ export function createProxy(options) {
563
628
  });
564
629
  return server;
565
630
  }
631
+ async function fetchModelAttempt(url, init, body, model, payment) {
632
+ let response = await fetchWithTimeout(url, { ...init, body: body || undefined }, payment.timeoutMs, `Proxy request for ${model}`);
633
+ if (response.status !== 402)
634
+ return response;
635
+ if (payment.chain === 'solana' && payment.solanaWallet) {
636
+ return handleSolanaPayment(response, url, payment.method, payment.headers, body, payment.solanaWallet.privateKey, payment.solanaWallet.address, payment.timeoutMs, model);
637
+ }
638
+ if (payment.baseWallet) {
639
+ return handleBasePayment(response, url, payment.method, payment.headers, body, payment.baseWallet.privateKey, payment.baseWallet.address, payment.timeoutMs, model);
640
+ }
641
+ return response;
642
+ }
643
+ /**
644
+ * Try each fallback model as a full x402 attempt:
645
+ * unpaid 402 probe, payment signing, then the paid provider call. The older
646
+ * flow only applied fallback to the probe, which meant a slow paid call could
647
+ * hang Franklin until the outer client gave up.
648
+ */
649
+ async function fetchWithPaymentFallback(url, init, originalBody, config, payment, onFallback) {
650
+ const failedModels = [];
651
+ let attempts = 0;
652
+ for (let i = 0; i < config.chain.length && attempts < config.maxRetries; i++) {
653
+ const model = config.chain[i];
654
+ const body = replaceModelInBody(originalBody, model);
655
+ try {
656
+ attempts++;
657
+ const response = await fetchModelAttempt(url, init, body, model, payment);
658
+ if (!config.retryOn.includes(response.status)) {
659
+ return {
660
+ response,
661
+ modelUsed: model,
662
+ bodyUsed: body,
663
+ fallbackUsed: i > 0,
664
+ attemptsCount: attempts,
665
+ failedModels,
666
+ };
667
+ }
668
+ try {
669
+ await response.body?.cancel();
670
+ }
671
+ catch { /* ignore */ }
672
+ failedModels.push(model);
673
+ const nextModel = config.chain[i + 1];
674
+ if (nextModel && onFallback) {
675
+ onFallback(model, response.status, nextModel);
676
+ }
677
+ if (i < config.chain.length - 1) {
678
+ await sleep(config.retryDelayMs);
679
+ }
680
+ }
681
+ catch (err) {
682
+ failedModels.push(model);
683
+ const nextModel = config.chain[i + 1];
684
+ if (nextModel && onFallback) {
685
+ onFallback(model, 0, nextModel);
686
+ }
687
+ log(`[fallback] ${model} request error: ${err instanceof Error ? err.message : String(err)}`);
688
+ if (i < config.chain.length - 1) {
689
+ await sleep(config.retryDelayMs);
690
+ }
691
+ }
692
+ }
693
+ throw new Error(`All models in fallback chain failed: ${failedModels.join(', ')}`);
694
+ }
695
+ function sleep(ms) {
696
+ return new Promise((resolve) => setTimeout(resolve, ms));
697
+ }
566
698
  // ======================================================================
567
699
  // Base (EIP-712) payment handler
568
700
  // ======================================================================
569
- async function handleBasePayment(response, url, method, headers, body, privateKey, fromAddress) {
701
+ async function handleBasePayment(response, url, method, headers, body, privateKey, fromAddress, timeoutMs = getProxyRequestTimeoutMs(), model = 'unknown') {
570
702
  const paymentHeader = await extractPaymentHeader(response);
571
703
  if (!paymentHeader) {
572
704
  throw new Error('402 Payment Required — wallet may need funding. Run: franklin balance');
@@ -579,19 +711,19 @@ async function handleBasePayment(response, url, method, headers, body, privateKe
579
711
  maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
580
712
  extra: details.extra,
581
713
  });
582
- return fetch(url, {
714
+ return fetchWithTimeout(url, {
583
715
  method,
584
716
  headers: {
585
717
  ...headers,
586
718
  'PAYMENT-SIGNATURE': paymentPayload,
587
719
  },
588
720
  body: body || undefined,
589
- });
721
+ }, timeoutMs, `Paid proxy request for ${model}`);
590
722
  }
591
723
  // ======================================================================
592
724
  // Solana payment handler
593
725
  // ======================================================================
594
- async function handleSolanaPayment(response, url, method, headers, body, privateKey, fromAddress) {
726
+ async function handleSolanaPayment(response, url, method, headers, body, privateKey, fromAddress, timeoutMs = getProxyRequestTimeoutMs(), model = 'unknown') {
595
727
  const paymentHeader = await extractPaymentHeader(response);
596
728
  if (!paymentHeader) {
597
729
  throw new Error('402 Payment Required — wallet may need funding. Run: franklin balance');
@@ -606,14 +738,14 @@ async function handleSolanaPayment(response, url, method, headers, body, private
606
738
  maxTimeoutSeconds: details.maxTimeoutSeconds || 300,
607
739
  extra: details.extra,
608
740
  });
609
- return fetch(url, {
741
+ return fetchWithTimeout(url, {
610
742
  method,
611
743
  headers: {
612
744
  ...headers,
613
745
  'PAYMENT-SIGNATURE': paymentPayload,
614
746
  },
615
747
  body: body || undefined,
616
- });
748
+ }, timeoutMs, `Paid proxy request for ${model}`);
617
749
  }
618
750
  export function classifyRequest(body) {
619
751
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.8.42",
3
+ "version": "3.8.44",
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": {