@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.
- package/dist/proxy/server.js +156 -24
- package/package.json +1 -1
package/dist/proxy/server.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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