@blockrun/clawrouter 0.12.56 → 0.12.61
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/README.md +93 -51
- package/dist/cli.js +456 -68
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +34 -0
- package/dist/index.js +601 -84
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/reinstall.sh +96 -17
- package/scripts/update.sh +93 -17
package/dist/index.js
CHANGED
|
@@ -32878,11 +32878,20 @@ var MODEL_ALIASES = {
|
|
|
32878
32878
|
"grok-fast": "xai/grok-4-fast-reasoning",
|
|
32879
32879
|
"grok-code": "deepseek/deepseek-chat",
|
|
32880
32880
|
// was grok-code-fast-1, delisted due to poor retention
|
|
32881
|
+
// Delisted model redirects — full model IDs that were previously valid but removed
|
|
32882
|
+
"grok-code-fast-1": "deepseek/deepseek-chat",
|
|
32883
|
+
// bare alias
|
|
32884
|
+
"xai/grok-code-fast-1": "deepseek/deepseek-chat",
|
|
32885
|
+
// delisted 2026-03-12
|
|
32886
|
+
"xai/grok-3-fast": "xai/grok-4-fast-reasoning",
|
|
32887
|
+
// delisted (too expensive)
|
|
32881
32888
|
// NVIDIA
|
|
32882
32889
|
nvidia: "nvidia/gpt-oss-120b",
|
|
32883
32890
|
"gpt-120b": "nvidia/gpt-oss-120b",
|
|
32884
32891
|
// MiniMax
|
|
32885
|
-
minimax: "minimax/minimax-m2.
|
|
32892
|
+
minimax: "minimax/minimax-m2.7",
|
|
32893
|
+
"minimax-m2.7": "minimax/minimax-m2.7",
|
|
32894
|
+
"minimax-m2.5": "minimax/minimax-m2.5",
|
|
32886
32895
|
// Z.AI GLM-5
|
|
32887
32896
|
glm: "zai/glm-5",
|
|
32888
32897
|
"glm-5": "zai/glm-5",
|
|
@@ -33388,6 +33397,18 @@ var BLOCKRUN_MODELS = [
|
|
|
33388
33397
|
toolCalling: true
|
|
33389
33398
|
},
|
|
33390
33399
|
// MiniMax
|
|
33400
|
+
{
|
|
33401
|
+
id: "minimax/minimax-m2.7",
|
|
33402
|
+
name: "MiniMax M2.7",
|
|
33403
|
+
version: "m2.7",
|
|
33404
|
+
inputPrice: 0.3,
|
|
33405
|
+
outputPrice: 1.2,
|
|
33406
|
+
contextWindow: 204800,
|
|
33407
|
+
maxOutput: 16384,
|
|
33408
|
+
reasoning: true,
|
|
33409
|
+
agentic: true,
|
|
33410
|
+
toolCalling: true
|
|
33411
|
+
},
|
|
33391
33412
|
{
|
|
33392
33413
|
id: "minimax/minimax-m2.5",
|
|
33393
33414
|
name: "MiniMax M2.5",
|
|
@@ -33534,10 +33555,10 @@ var blockrunProvider = {
|
|
|
33534
33555
|
// src/proxy.ts
|
|
33535
33556
|
import { createServer } from "http";
|
|
33536
33557
|
import { finished } from "stream";
|
|
33537
|
-
import { homedir as
|
|
33538
|
-
import { join as
|
|
33558
|
+
import { homedir as homedir5 } from "os";
|
|
33559
|
+
import { join as join8 } from "path";
|
|
33539
33560
|
import { mkdir as mkdir3, writeFile as writeFile2, readFile, stat as fsStat } from "fs/promises";
|
|
33540
|
-
import { readFileSync, existsSync } from "fs";
|
|
33561
|
+
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
33541
33562
|
|
|
33542
33563
|
// node_modules/viem/_esm/utils/getAction.js
|
|
33543
33564
|
function getAction(client, actionFn, name) {
|
|
@@ -43487,6 +43508,11 @@ function filterByVision(models, hasVision, supportsVision2) {
|
|
|
43487
43508
|
const filtered = models.filter(supportsVision2);
|
|
43488
43509
|
return filtered.length > 0 ? filtered : models;
|
|
43489
43510
|
}
|
|
43511
|
+
function filterByExcludeList(models, excludeList) {
|
|
43512
|
+
if (excludeList.size === 0) return models;
|
|
43513
|
+
const filtered = models.filter((m) => !excludeList.has(m));
|
|
43514
|
+
return filtered.length > 0 ? filtered : models;
|
|
43515
|
+
}
|
|
43490
43516
|
function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
|
|
43491
43517
|
const fullChain = getFallbackChain(tier, tierConfigs);
|
|
43492
43518
|
const filtered = fullChain.filter((modelId) => {
|
|
@@ -46642,7 +46668,8 @@ var SessionStore = class {
|
|
|
46642
46668
|
requestCount: 1,
|
|
46643
46669
|
recentHashes: [],
|
|
46644
46670
|
strikes: 0,
|
|
46645
|
-
escalated: false
|
|
46671
|
+
escalated: false,
|
|
46672
|
+
sessionCostMicros: 0n
|
|
46646
46673
|
});
|
|
46647
46674
|
}
|
|
46648
46675
|
}
|
|
@@ -46731,6 +46758,39 @@ var SessionStore = class {
|
|
|
46731
46758
|
entry.escalated = true;
|
|
46732
46759
|
return { model: nextConfig.primary, tier: nextTier };
|
|
46733
46760
|
}
|
|
46761
|
+
/**
|
|
46762
|
+
* Add cost to a session's running total for maxCostPerRun tracking.
|
|
46763
|
+
* Cost is in USDC 6-decimal units (micros).
|
|
46764
|
+
* Creates a cost-tracking-only entry if none exists (e.g., explicit model requests
|
|
46765
|
+
* that never go through the routing path).
|
|
46766
|
+
*/
|
|
46767
|
+
addSessionCost(sessionId, additionalMicros) {
|
|
46768
|
+
let entry = this.sessions.get(sessionId);
|
|
46769
|
+
if (!entry) {
|
|
46770
|
+
const now = Date.now();
|
|
46771
|
+
entry = {
|
|
46772
|
+
model: "",
|
|
46773
|
+
tier: "DIRECT",
|
|
46774
|
+
createdAt: now,
|
|
46775
|
+
lastUsedAt: now,
|
|
46776
|
+
requestCount: 0,
|
|
46777
|
+
recentHashes: [],
|
|
46778
|
+
strikes: 0,
|
|
46779
|
+
escalated: false,
|
|
46780
|
+
sessionCostMicros: 0n
|
|
46781
|
+
};
|
|
46782
|
+
this.sessions.set(sessionId, entry);
|
|
46783
|
+
}
|
|
46784
|
+
entry.sessionCostMicros += additionalMicros;
|
|
46785
|
+
}
|
|
46786
|
+
/**
|
|
46787
|
+
* Get the total accumulated cost for a session in USD.
|
|
46788
|
+
*/
|
|
46789
|
+
getSessionCostUsd(sessionId) {
|
|
46790
|
+
const entry = this.sessions.get(sessionId);
|
|
46791
|
+
if (!entry) return 0;
|
|
46792
|
+
return Number(entry.sessionCostMicros) / 1e6;
|
|
46793
|
+
}
|
|
46734
46794
|
/**
|
|
46735
46795
|
* Stop the cleanup interval.
|
|
46736
46796
|
*/
|
|
@@ -46798,6 +46858,54 @@ async function checkForUpdates() {
|
|
|
46798
46858
|
}
|
|
46799
46859
|
}
|
|
46800
46860
|
|
|
46861
|
+
// src/exclude-models.ts
|
|
46862
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
46863
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
46864
|
+
import { homedir as homedir4 } from "os";
|
|
46865
|
+
var DEFAULT_FILE_PATH = join7(
|
|
46866
|
+
homedir4(),
|
|
46867
|
+
".openclaw",
|
|
46868
|
+
"blockrun",
|
|
46869
|
+
"exclude-models.json"
|
|
46870
|
+
);
|
|
46871
|
+
function loadExcludeList(filePath = DEFAULT_FILE_PATH) {
|
|
46872
|
+
try {
|
|
46873
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
46874
|
+
const arr = JSON.parse(raw);
|
|
46875
|
+
if (Array.isArray(arr)) {
|
|
46876
|
+
return new Set(arr.filter((x) => typeof x === "string"));
|
|
46877
|
+
}
|
|
46878
|
+
return /* @__PURE__ */ new Set();
|
|
46879
|
+
} catch {
|
|
46880
|
+
return /* @__PURE__ */ new Set();
|
|
46881
|
+
}
|
|
46882
|
+
}
|
|
46883
|
+
function saveExcludeList(set, filePath) {
|
|
46884
|
+
const sorted = [...set].sort();
|
|
46885
|
+
const dir = dirname2(filePath);
|
|
46886
|
+
mkdirSync(dir, { recursive: true });
|
|
46887
|
+
writeFileSync(filePath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
|
|
46888
|
+
}
|
|
46889
|
+
function addExclusion(model, filePath = DEFAULT_FILE_PATH) {
|
|
46890
|
+
const resolved = resolveModelAlias(model);
|
|
46891
|
+
const set = loadExcludeList(filePath);
|
|
46892
|
+
set.add(resolved);
|
|
46893
|
+
saveExcludeList(set, filePath);
|
|
46894
|
+
return resolved;
|
|
46895
|
+
}
|
|
46896
|
+
function removeExclusion(model, filePath = DEFAULT_FILE_PATH) {
|
|
46897
|
+
const resolved = resolveModelAlias(model);
|
|
46898
|
+
const set = loadExcludeList(filePath);
|
|
46899
|
+
const had = set.delete(resolved);
|
|
46900
|
+
if (had) {
|
|
46901
|
+
saveExcludeList(set, filePath);
|
|
46902
|
+
}
|
|
46903
|
+
return had;
|
|
46904
|
+
}
|
|
46905
|
+
function clearExclusions(filePath = DEFAULT_FILE_PATH) {
|
|
46906
|
+
saveExcludeList(/* @__PURE__ */ new Set(), filePath);
|
|
46907
|
+
}
|
|
46908
|
+
|
|
46801
46909
|
// src/config.ts
|
|
46802
46910
|
var DEFAULT_PORT = 8402;
|
|
46803
46911
|
var PROXY_PORT = (() => {
|
|
@@ -46982,7 +47090,7 @@ ${lines.join("\n")}`;
|
|
|
46982
47090
|
// src/proxy.ts
|
|
46983
47091
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
46984
47092
|
var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
|
|
46985
|
-
var IMAGE_DIR =
|
|
47093
|
+
var IMAGE_DIR = join8(homedir5(), ".openclaw", "blockrun", "images");
|
|
46986
47094
|
var AUTO_MODEL = "blockrun/auto";
|
|
46987
47095
|
var ROUTING_PROFILES = /* @__PURE__ */ new Set([
|
|
46988
47096
|
"blockrun/free",
|
|
@@ -47006,9 +47114,11 @@ var MAX_MESSAGES = 200;
|
|
|
47006
47114
|
var CONTEXT_LIMIT_KB = 5120;
|
|
47007
47115
|
var HEARTBEAT_INTERVAL_MS = 2e3;
|
|
47008
47116
|
var DEFAULT_REQUEST_TIMEOUT_MS = 18e4;
|
|
47117
|
+
var PER_MODEL_TIMEOUT_MS = 6e4;
|
|
47009
47118
|
var MAX_FALLBACK_ATTEMPTS = 5;
|
|
47010
47119
|
var HEALTH_CHECK_TIMEOUT_MS = 2e3;
|
|
47011
47120
|
var RATE_LIMIT_COOLDOWN_MS = 6e4;
|
|
47121
|
+
var OVERLOAD_COOLDOWN_MS = 15e3;
|
|
47012
47122
|
var PORT_RETRY_ATTEMPTS = 5;
|
|
47013
47123
|
var PORT_RETRY_DELAY_MS = 1e3;
|
|
47014
47124
|
var MODEL_BODY_READ_TIMEOUT_MS = 3e5;
|
|
@@ -47158,7 +47268,41 @@ function transformPaymentError(errorBody) {
|
|
|
47158
47268
|
}
|
|
47159
47269
|
return errorBody;
|
|
47160
47270
|
}
|
|
47271
|
+
function categorizeError(status, body) {
|
|
47272
|
+
if (status === 401) return "auth_failure";
|
|
47273
|
+
if (status === 402) return "payment_error";
|
|
47274
|
+
if (status === 403) {
|
|
47275
|
+
if (/plan.*limit|quota.*exceeded|subscription|allowance/i.test(body))
|
|
47276
|
+
return "quota_exceeded";
|
|
47277
|
+
return "auth_failure";
|
|
47278
|
+
}
|
|
47279
|
+
if (status === 429) return "rate_limited";
|
|
47280
|
+
if (status === 529) return "overloaded";
|
|
47281
|
+
if (status === 503 && /overload|capacity|too.*many.*request/i.test(body)) return "overloaded";
|
|
47282
|
+
if (status >= 500) return "server_error";
|
|
47283
|
+
if (status === 400 || status === 413) {
|
|
47284
|
+
if (PROVIDER_ERROR_PATTERNS.some((p) => p.test(body))) return "config_error";
|
|
47285
|
+
return null;
|
|
47286
|
+
}
|
|
47287
|
+
return null;
|
|
47288
|
+
}
|
|
47161
47289
|
var rateLimitedModels = /* @__PURE__ */ new Map();
|
|
47290
|
+
var overloadedModels = /* @__PURE__ */ new Map();
|
|
47291
|
+
var perProviderErrors = /* @__PURE__ */ new Map();
|
|
47292
|
+
function recordProviderError(modelId, category) {
|
|
47293
|
+
if (!perProviderErrors.has(modelId)) {
|
|
47294
|
+
perProviderErrors.set(modelId, {
|
|
47295
|
+
auth_failure: 0,
|
|
47296
|
+
quota_exceeded: 0,
|
|
47297
|
+
rate_limited: 0,
|
|
47298
|
+
overloaded: 0,
|
|
47299
|
+
server_error: 0,
|
|
47300
|
+
payment_error: 0,
|
|
47301
|
+
config_error: 0
|
|
47302
|
+
});
|
|
47303
|
+
}
|
|
47304
|
+
perProviderErrors.get(modelId)[category]++;
|
|
47305
|
+
}
|
|
47162
47306
|
function isRateLimited(modelId) {
|
|
47163
47307
|
const hitTime = rateLimitedModels.get(modelId);
|
|
47164
47308
|
if (!hitTime) return false;
|
|
@@ -47173,17 +47317,30 @@ function markRateLimited(modelId) {
|
|
|
47173
47317
|
rateLimitedModels.set(modelId, Date.now());
|
|
47174
47318
|
console.log(`[ClawRouter] Model ${modelId} rate-limited, will deprioritize for 60s`);
|
|
47175
47319
|
}
|
|
47320
|
+
function markOverloaded(modelId) {
|
|
47321
|
+
overloadedModels.set(modelId, Date.now());
|
|
47322
|
+
console.log(`[ClawRouter] Model ${modelId} overloaded, will deprioritize for 15s`);
|
|
47323
|
+
}
|
|
47324
|
+
function isOverloaded(modelId) {
|
|
47325
|
+
const hitTime = overloadedModels.get(modelId);
|
|
47326
|
+
if (!hitTime) return false;
|
|
47327
|
+
if (Date.now() - hitTime >= OVERLOAD_COOLDOWN_MS) {
|
|
47328
|
+
overloadedModels.delete(modelId);
|
|
47329
|
+
return false;
|
|
47330
|
+
}
|
|
47331
|
+
return true;
|
|
47332
|
+
}
|
|
47176
47333
|
function prioritizeNonRateLimited(models) {
|
|
47177
47334
|
const available = [];
|
|
47178
|
-
const
|
|
47335
|
+
const degraded = [];
|
|
47179
47336
|
for (const model of models) {
|
|
47180
|
-
if (isRateLimited(model)) {
|
|
47181
|
-
|
|
47337
|
+
if (isRateLimited(model) || isOverloaded(model)) {
|
|
47338
|
+
degraded.push(model);
|
|
47182
47339
|
} else {
|
|
47183
47340
|
available.push(model);
|
|
47184
47341
|
}
|
|
47185
47342
|
}
|
|
47186
|
-
return [...available, ...
|
|
47343
|
+
return [...available, ...degraded];
|
|
47187
47344
|
}
|
|
47188
47345
|
function canWrite(res) {
|
|
47189
47346
|
return !res.writableEnded && !res.destroyed && res.socket !== null && !res.socket.destroyed && res.socket.writable;
|
|
@@ -47318,37 +47475,6 @@ function detectDegradedSuccessResponse(body) {
|
|
|
47318
47475
|
}
|
|
47319
47476
|
return void 0;
|
|
47320
47477
|
}
|
|
47321
|
-
var FALLBACK_STATUS_CODES = [
|
|
47322
|
-
400,
|
|
47323
|
-
// Bad request - sometimes used for billing errors
|
|
47324
|
-
401,
|
|
47325
|
-
// Unauthorized - provider API key issues
|
|
47326
|
-
402,
|
|
47327
|
-
// Payment required - but from upstream, not x402
|
|
47328
|
-
403,
|
|
47329
|
-
// Forbidden - provider restrictions
|
|
47330
|
-
413,
|
|
47331
|
-
// Payload too large - request exceeds model's context limit
|
|
47332
|
-
429,
|
|
47333
|
-
// Rate limited
|
|
47334
|
-
500,
|
|
47335
|
-
// Internal server error
|
|
47336
|
-
502,
|
|
47337
|
-
// Bad gateway
|
|
47338
|
-
503,
|
|
47339
|
-
// Service unavailable
|
|
47340
|
-
504
|
|
47341
|
-
// Gateway timeout
|
|
47342
|
-
];
|
|
47343
|
-
function isProviderError(status, body) {
|
|
47344
|
-
if (!FALLBACK_STATUS_CODES.includes(status)) {
|
|
47345
|
-
return false;
|
|
47346
|
-
}
|
|
47347
|
-
if (status >= 500) {
|
|
47348
|
-
return true;
|
|
47349
|
-
}
|
|
47350
|
-
return PROVIDER_ERROR_PATTERNS.some((pattern) => pattern.test(body));
|
|
47351
|
-
}
|
|
47352
47478
|
var VALID_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool", "function"]);
|
|
47353
47479
|
var ROLE_MAPPINGS = {
|
|
47354
47480
|
developer: "system",
|
|
@@ -47632,7 +47758,7 @@ async function proxyPartnerRequest(req, res, apiBase, payFetch) {
|
|
|
47632
47758
|
});
|
|
47633
47759
|
}
|
|
47634
47760
|
function readImageFileAsDataUri(filePath) {
|
|
47635
|
-
const resolved = filePath.startsWith("~/") ?
|
|
47761
|
+
const resolved = filePath.startsWith("~/") ? join8(homedir5(), filePath.slice(2)) : filePath;
|
|
47636
47762
|
if (!existsSync(resolved)) {
|
|
47637
47763
|
throw new Error(`Image file not found: ${resolved}`);
|
|
47638
47764
|
}
|
|
@@ -47644,7 +47770,7 @@ function readImageFileAsDataUri(filePath) {
|
|
|
47644
47770
|
webp: "image/webp"
|
|
47645
47771
|
};
|
|
47646
47772
|
const mime = mimeMap[ext] ?? "image/png";
|
|
47647
|
-
const data =
|
|
47773
|
+
const data = readFileSync2(resolved);
|
|
47648
47774
|
return `data:${mime};base64,${data.toString("base64")}`;
|
|
47649
47775
|
}
|
|
47650
47776
|
async function uploadDataUriToHost(dataUri) {
|
|
@@ -47762,7 +47888,9 @@ async function startProxy(options) {
|
|
|
47762
47888
|
skipPreAuth: paymentChain === "solana"
|
|
47763
47889
|
});
|
|
47764
47890
|
let balanceMonitor;
|
|
47765
|
-
if (
|
|
47891
|
+
if (options._balanceMonitorOverride) {
|
|
47892
|
+
balanceMonitor = options._balanceMonitorOverride;
|
|
47893
|
+
} else if (paymentChain === "solana" && solanaAddress) {
|
|
47766
47894
|
const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
|
|
47767
47895
|
balanceMonitor = new SolanaBalanceMonitor2(solanaAddress);
|
|
47768
47896
|
} else {
|
|
@@ -47854,7 +47982,16 @@ async function startProxy(options) {
|
|
|
47854
47982
|
"Content-Type": "application/json",
|
|
47855
47983
|
"Cache-Control": "no-cache"
|
|
47856
47984
|
});
|
|
47857
|
-
res.end(
|
|
47985
|
+
res.end(
|
|
47986
|
+
JSON.stringify(
|
|
47987
|
+
{
|
|
47988
|
+
...stats,
|
|
47989
|
+
providerErrors: Object.fromEntries(perProviderErrors)
|
|
47990
|
+
},
|
|
47991
|
+
null,
|
|
47992
|
+
2
|
|
47993
|
+
)
|
|
47994
|
+
);
|
|
47858
47995
|
} catch (err) {
|
|
47859
47996
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
47860
47997
|
res.end(
|
|
@@ -47878,7 +48015,7 @@ async function startProxy(options) {
|
|
|
47878
48015
|
res.end("Bad request");
|
|
47879
48016
|
return;
|
|
47880
48017
|
}
|
|
47881
|
-
const filePath =
|
|
48018
|
+
const filePath = join8(IMAGE_DIR, filename);
|
|
47882
48019
|
try {
|
|
47883
48020
|
const s3 = await fsStat(filePath);
|
|
47884
48021
|
if (!s3.isFile()) throw new Error("not a file");
|
|
@@ -47937,7 +48074,7 @@ async function startProxy(options) {
|
|
|
47937
48074
|
const [, mimeType, b64] = dataUriMatch;
|
|
47938
48075
|
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
|
|
47939
48076
|
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
47940
|
-
await writeFile2(
|
|
48077
|
+
await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
|
|
47941
48078
|
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
47942
48079
|
console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
|
|
47943
48080
|
} else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
|
|
@@ -47948,7 +48085,7 @@ async function startProxy(options) {
|
|
|
47948
48085
|
const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
47949
48086
|
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
47950
48087
|
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
47951
|
-
await writeFile2(
|
|
48088
|
+
await writeFile2(join8(IMAGE_DIR, filename), buf);
|
|
47952
48089
|
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
47953
48090
|
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
47954
48091
|
}
|
|
@@ -48037,7 +48174,7 @@ async function startProxy(options) {
|
|
|
48037
48174
|
const [, mimeType, b64] = dataUriMatch;
|
|
48038
48175
|
const ext = mimeType === "image/jpeg" ? "jpg" : mimeType.split("/")[1] ?? "png";
|
|
48039
48176
|
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
48040
|
-
await writeFile2(
|
|
48177
|
+
await writeFile2(join8(IMAGE_DIR, filename), Buffer.from(b64, "base64"));
|
|
48041
48178
|
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
48042
48179
|
console.log(`[ClawRouter] Image saved \u2192 ${img.url}`);
|
|
48043
48180
|
} else if (img.url?.startsWith("https://") || img.url?.startsWith("http://")) {
|
|
@@ -48048,7 +48185,7 @@ async function startProxy(options) {
|
|
|
48048
48185
|
const ext = contentType.includes("jpeg") || contentType.includes("jpg") ? "jpg" : contentType.includes("webp") ? "webp" : "png";
|
|
48049
48186
|
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}.${ext}`;
|
|
48050
48187
|
const buf = Buffer.from(await imgResp.arrayBuffer());
|
|
48051
|
-
await writeFile2(
|
|
48188
|
+
await writeFile2(join8(IMAGE_DIR, filename), buf);
|
|
48052
48189
|
img.url = `http://localhost:${port2}/images/${filename}`;
|
|
48053
48190
|
console.log(`[ClawRouter] Image downloaded & saved \u2192 ${img.url}`);
|
|
48054
48191
|
}
|
|
@@ -48297,12 +48434,13 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
48297
48434
|
if (response.status !== 200) {
|
|
48298
48435
|
const errorBodyChunks = await readBodyWithTimeout(response.body, ERROR_BODY_READ_TIMEOUT_MS);
|
|
48299
48436
|
const errorBody = Buffer.concat(errorBodyChunks).toString();
|
|
48300
|
-
const
|
|
48437
|
+
const category = categorizeError(response.status, errorBody);
|
|
48301
48438
|
return {
|
|
48302
48439
|
success: false,
|
|
48303
48440
|
errorBody,
|
|
48304
48441
|
errorStatus: response.status,
|
|
48305
|
-
isProviderError:
|
|
48442
|
+
isProviderError: category !== null,
|
|
48443
|
+
errorCategory: category ?? void 0
|
|
48306
48444
|
};
|
|
48307
48445
|
}
|
|
48308
48446
|
const contentType = response.headers.get("content-type") || "";
|
|
@@ -48355,8 +48493,11 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
48355
48493
|
let maxTokens = 4096;
|
|
48356
48494
|
let routingProfile = null;
|
|
48357
48495
|
let balanceFallbackNotice;
|
|
48496
|
+
let budgetDowngradeNotice;
|
|
48497
|
+
let budgetDowngradeHeaderMode;
|
|
48358
48498
|
let accumulatedContent = "";
|
|
48359
48499
|
let responseInputTokens;
|
|
48500
|
+
let responseOutputTokens;
|
|
48360
48501
|
const isChatCompletion = req.url?.includes("/chat/completions");
|
|
48361
48502
|
const sessionId = getSessionId(req.headers);
|
|
48362
48503
|
let effectiveSessionId = sessionId;
|
|
@@ -48369,6 +48510,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
48369
48510
|
let bodyModified = false;
|
|
48370
48511
|
const parsedMessages = Array.isArray(parsed.messages) ? parsed.messages : [];
|
|
48371
48512
|
const lastUserMsg = [...parsedMessages].reverse().find((m) => m.role === "user");
|
|
48513
|
+
hasTools = Array.isArray(parsed.tools) && parsed.tools.length > 0;
|
|
48372
48514
|
const rawLastContent = lastUserMsg?.content;
|
|
48373
48515
|
const lastContent = typeof rawLastContent === "string" ? rawLastContent : Array.isArray(rawLastContent) ? rawLastContent.filter((b) => b.type === "text").map((b) => b.text ?? "").join(" ") : "";
|
|
48374
48516
|
if (sessionId && parsedMessages.length > 0) {
|
|
@@ -49025,6 +49167,9 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49025
49167
|
options.onRouted?.(routingDecision);
|
|
49026
49168
|
}
|
|
49027
49169
|
}
|
|
49170
|
+
if (!effectiveSessionId && parsedMessages.length > 0) {
|
|
49171
|
+
effectiveSessionId = deriveSessionId(parsedMessages);
|
|
49172
|
+
}
|
|
49028
49173
|
if (bodyModified) {
|
|
49029
49174
|
body = Buffer.from(JSON.stringify(parsed));
|
|
49030
49175
|
}
|
|
@@ -49115,7 +49260,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49115
49260
|
}
|
|
49116
49261
|
deduplicator.markInflight(dedupKey);
|
|
49117
49262
|
let estimatedCostMicros;
|
|
49118
|
-
|
|
49263
|
+
let isFreeModel = modelId === FREE_MODEL;
|
|
49119
49264
|
if (modelId && !options.skipBalanceCheck && !isFreeModel) {
|
|
49120
49265
|
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
49121
49266
|
if (estimated) {
|
|
@@ -49128,6 +49273,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49128
49273
|
`[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
|
|
49129
49274
|
);
|
|
49130
49275
|
modelId = FREE_MODEL;
|
|
49276
|
+
isFreeModel = true;
|
|
49131
49277
|
const parsed = JSON.parse(body.toString());
|
|
49132
49278
|
parsed.model = FREE_MODEL;
|
|
49133
49279
|
body = Buffer.from(JSON.stringify(parsed));
|
|
@@ -49154,6 +49300,89 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49154
49300
|
}
|
|
49155
49301
|
}
|
|
49156
49302
|
}
|
|
49303
|
+
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "strict") {
|
|
49304
|
+
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
49305
|
+
const thisReqEstStr = estimatedCostMicros !== void 0 ? estimatedCostMicros.toString() : modelId ? estimateAmount(modelId, body.length, maxTokens) : void 0;
|
|
49306
|
+
const thisReqEstUsd = thisReqEstStr ? Number(thisReqEstStr) / 1e6 : 0;
|
|
49307
|
+
const projectedCostUsd = runCostUsd + thisReqEstUsd;
|
|
49308
|
+
if (projectedCostUsd > options.maxCostPerRunUsd) {
|
|
49309
|
+
console.log(
|
|
49310
|
+
`[ClawRouter] Cost cap exceeded for session ${effectiveSessionId.slice(0, 8)}...: projected $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) > $${options.maxCostPerRunUsd} limit`
|
|
49311
|
+
);
|
|
49312
|
+
res.writeHead(429, {
|
|
49313
|
+
"Content-Type": "application/json",
|
|
49314
|
+
"X-ClawRouter-Cost-Cap-Exceeded": "1"
|
|
49315
|
+
});
|
|
49316
|
+
res.end(
|
|
49317
|
+
JSON.stringify({
|
|
49318
|
+
error: {
|
|
49319
|
+
message: `ClawRouter cost cap exceeded: projected spend $${projectedCostUsd.toFixed(4)} (spent $${runCostUsd.toFixed(4)} + est $${thisReqEstUsd.toFixed(4)}) would exceed limit $${options.maxCostPerRunUsd}`,
|
|
49320
|
+
type: "cost_cap_exceeded",
|
|
49321
|
+
code: "cost_cap_exceeded"
|
|
49322
|
+
}
|
|
49323
|
+
})
|
|
49324
|
+
);
|
|
49325
|
+
deduplicator.removeInflight(dedupKey);
|
|
49326
|
+
return;
|
|
49327
|
+
}
|
|
49328
|
+
}
|
|
49329
|
+
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
|
|
49330
|
+
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
49331
|
+
const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
|
|
49332
|
+
const isComplexOrAgentic = hasTools || routingDecision?.tier === "COMPLEX" || routingDecision?.tier === "REASONING";
|
|
49333
|
+
if (isComplexOrAgentic) {
|
|
49334
|
+
const canAffordAnyNonFreeModel = BLOCKRUN_MODELS.some((m) => {
|
|
49335
|
+
if (m.id === FREE_MODEL) return false;
|
|
49336
|
+
const est = estimateAmount(m.id, body.length, maxTokens);
|
|
49337
|
+
return est !== void 0 && Number(est) / 1e6 <= remainingUsd;
|
|
49338
|
+
});
|
|
49339
|
+
if (!canAffordAnyNonFreeModel) {
|
|
49340
|
+
console.log(
|
|
49341
|
+
`[ClawRouter] Budget insufficient for agentic/complex session ${effectiveSessionId.slice(0, 8)}...: $${Math.max(0, remainingUsd).toFixed(4)} remaining \u2014 blocking (silent downgrade would corrupt tool/complex responses)`
|
|
49342
|
+
);
|
|
49343
|
+
res.writeHead(429, {
|
|
49344
|
+
"Content-Type": "application/json",
|
|
49345
|
+
"X-ClawRouter-Cost-Cap-Exceeded": "1",
|
|
49346
|
+
"X-ClawRouter-Budget-Mode": "blocked"
|
|
49347
|
+
});
|
|
49348
|
+
res.end(
|
|
49349
|
+
JSON.stringify({
|
|
49350
|
+
error: {
|
|
49351
|
+
message: `ClawRouter budget exhausted: $${Math.max(0, remainingUsd).toFixed(4)} remaining (limit: $${options.maxCostPerRunUsd}). Increase maxCostPerRun to continue.`,
|
|
49352
|
+
type: "cost_cap_exceeded",
|
|
49353
|
+
code: "budget_exhausted"
|
|
49354
|
+
}
|
|
49355
|
+
})
|
|
49356
|
+
);
|
|
49357
|
+
deduplicator.removeInflight(dedupKey);
|
|
49358
|
+
return;
|
|
49359
|
+
}
|
|
49360
|
+
} else if (!routingDecision && modelId && modelId !== FREE_MODEL) {
|
|
49361
|
+
const est = estimateAmount(modelId, body.length, maxTokens);
|
|
49362
|
+
const canAfford = !est || Number(est) / 1e6 <= remainingUsd;
|
|
49363
|
+
if (!canAfford) {
|
|
49364
|
+
console.log(
|
|
49365
|
+
`[ClawRouter] Budget insufficient for explicit model ${modelId} in session ${effectiveSessionId.slice(0, 8)}...: $${Math.max(0, remainingUsd).toFixed(4)} remaining \u2014 blocking (user explicitly chose ${modelId})`
|
|
49366
|
+
);
|
|
49367
|
+
res.writeHead(429, {
|
|
49368
|
+
"Content-Type": "application/json",
|
|
49369
|
+
"X-ClawRouter-Cost-Cap-Exceeded": "1",
|
|
49370
|
+
"X-ClawRouter-Budget-Mode": "blocked"
|
|
49371
|
+
});
|
|
49372
|
+
res.end(
|
|
49373
|
+
JSON.stringify({
|
|
49374
|
+
error: {
|
|
49375
|
+
message: `ClawRouter budget exhausted: $${Math.max(0, remainingUsd).toFixed(4)} remaining (limit: $${options.maxCostPerRunUsd}). Increase maxCostPerRun to continue using ${modelId}.`,
|
|
49376
|
+
type: "cost_cap_exceeded",
|
|
49377
|
+
code: "budget_exhausted"
|
|
49378
|
+
}
|
|
49379
|
+
})
|
|
49380
|
+
);
|
|
49381
|
+
deduplicator.removeInflight(dedupKey);
|
|
49382
|
+
return;
|
|
49383
|
+
}
|
|
49384
|
+
}
|
|
49385
|
+
}
|
|
49157
49386
|
let heartbeatInterval;
|
|
49158
49387
|
let headersSentEarly = false;
|
|
49159
49388
|
if (isStreaming) {
|
|
@@ -49198,10 +49427,11 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49198
49427
|
}
|
|
49199
49428
|
});
|
|
49200
49429
|
const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
49201
|
-
const
|
|
49202
|
-
const timeoutId = setTimeout(() =>
|
|
49430
|
+
const globalController = new AbortController();
|
|
49431
|
+
const timeoutId = setTimeout(() => globalController.abort(), timeoutMs);
|
|
49203
49432
|
try {
|
|
49204
49433
|
let modelsToTry;
|
|
49434
|
+
const excludeList = options.excludeModels ?? loadExcludeList();
|
|
49205
49435
|
if (routingDecision) {
|
|
49206
49436
|
const estimatedInputTokens = Math.ceil(body.length / 4);
|
|
49207
49437
|
const estimatedTotalTokens = estimatedInputTokens + maxTokens;
|
|
@@ -49219,8 +49449,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49219
49449
|
`[ClawRouter] Context filter (~${estimatedTotalTokens} tokens): excluded ${contextExcluded.join(", ")}`
|
|
49220
49450
|
);
|
|
49221
49451
|
}
|
|
49222
|
-
|
|
49223
|
-
const
|
|
49452
|
+
const excludeFiltered = filterByExcludeList(contextFiltered, excludeList);
|
|
49453
|
+
const excludeExcluded = contextFiltered.filter((m) => !excludeFiltered.includes(m));
|
|
49454
|
+
if (excludeExcluded.length > 0) {
|
|
49455
|
+
console.log(
|
|
49456
|
+
`[ClawRouter] Exclude filter: excluded ${excludeExcluded.join(", ")} (user preference)`
|
|
49457
|
+
);
|
|
49458
|
+
}
|
|
49459
|
+
let toolFiltered = filterByToolCalling(excludeFiltered, hasTools, supportsToolCalling);
|
|
49460
|
+
const toolExcluded = excludeFiltered.filter((m) => !toolFiltered.includes(m));
|
|
49224
49461
|
if (toolExcluded.length > 0) {
|
|
49225
49462
|
console.log(
|
|
49226
49463
|
`[ClawRouter] Tool-calling filter: excluded ${toolExcluded.join(", ")} (no structured function call support)`
|
|
@@ -49253,16 +49490,86 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49253
49490
|
} else {
|
|
49254
49491
|
modelsToTry = modelId ? [modelId] : [];
|
|
49255
49492
|
}
|
|
49256
|
-
if (!hasTools && !modelsToTry.includes(FREE_MODEL)) {
|
|
49493
|
+
if (!hasTools && !modelsToTry.includes(FREE_MODEL) && !excludeList.has(FREE_MODEL)) {
|
|
49257
49494
|
modelsToTry.push(FREE_MODEL);
|
|
49258
49495
|
}
|
|
49496
|
+
if (options.maxCostPerRunUsd && effectiveSessionId && !isFreeModel && (options.maxCostPerRunMode ?? "graceful") === "graceful") {
|
|
49497
|
+
const runCostUsd = sessionStore.getSessionCostUsd(effectiveSessionId);
|
|
49498
|
+
const remainingUsd = options.maxCostPerRunUsd - runCostUsd;
|
|
49499
|
+
const beforeFilter = [...modelsToTry];
|
|
49500
|
+
modelsToTry = modelsToTry.filter((m) => {
|
|
49501
|
+
if (m === FREE_MODEL) return true;
|
|
49502
|
+
const est = estimateAmount(m, body.length, maxTokens);
|
|
49503
|
+
if (!est) return true;
|
|
49504
|
+
return Number(est) / 1e6 <= remainingUsd;
|
|
49505
|
+
});
|
|
49506
|
+
const excluded = beforeFilter.filter((m) => !modelsToTry.includes(m));
|
|
49507
|
+
const isComplexOrAgenticFilter = hasTools || routingDecision?.tier === "COMPLEX" || routingDecision?.tier === "REASONING" || routingDecision === void 0;
|
|
49508
|
+
const filteredToFreeOnly = modelsToTry.length > 0 && modelsToTry.every((m) => m === FREE_MODEL);
|
|
49509
|
+
if (isComplexOrAgenticFilter && filteredToFreeOnly) {
|
|
49510
|
+
const budgetSummary = `$${Math.max(0, remainingUsd).toFixed(4)} remaining (limit: $${options.maxCostPerRunUsd})`;
|
|
49511
|
+
console.log(
|
|
49512
|
+
`[ClawRouter] Budget filter left only free model for complex/agentic session \u2014 blocking (${budgetSummary})`
|
|
49513
|
+
);
|
|
49514
|
+
const errPayload = JSON.stringify({
|
|
49515
|
+
error: {
|
|
49516
|
+
message: `ClawRouter budget exhausted: remaining budget (${budgetSummary}) cannot support a complex/tool request. Increase maxCostPerRun to continue.`,
|
|
49517
|
+
type: "cost_cap_exceeded",
|
|
49518
|
+
code: "budget_exhausted"
|
|
49519
|
+
}
|
|
49520
|
+
});
|
|
49521
|
+
if (heartbeatInterval) clearInterval(heartbeatInterval);
|
|
49522
|
+
if (headersSentEarly) {
|
|
49523
|
+
safeWrite(res, `data: ${errPayload}
|
|
49524
|
+
|
|
49525
|
+
data: [DONE]
|
|
49526
|
+
|
|
49527
|
+
`);
|
|
49528
|
+
res.end();
|
|
49529
|
+
} else {
|
|
49530
|
+
res.writeHead(429, {
|
|
49531
|
+
"Content-Type": "application/json",
|
|
49532
|
+
"X-ClawRouter-Cost-Cap-Exceeded": "1",
|
|
49533
|
+
"X-ClawRouter-Budget-Mode": "blocked"
|
|
49534
|
+
});
|
|
49535
|
+
res.end(errPayload);
|
|
49536
|
+
}
|
|
49537
|
+
deduplicator.removeInflight(dedupKey);
|
|
49538
|
+
return;
|
|
49539
|
+
}
|
|
49540
|
+
if (excluded.length > 0) {
|
|
49541
|
+
const budgetSummary = remainingUsd > 0 ? `$${remainingUsd.toFixed(4)} remaining` : `budget exhausted ($${runCostUsd.toFixed(4)}/$${options.maxCostPerRunUsd})`;
|
|
49542
|
+
console.log(
|
|
49543
|
+
`[ClawRouter] Budget downgrade (${budgetSummary}): excluded ${excluded.join(", ")}`
|
|
49544
|
+
);
|
|
49545
|
+
const fromModel = excluded[0];
|
|
49546
|
+
const usingFree = modelsToTry.length === 1 && modelsToTry[0] === FREE_MODEL;
|
|
49547
|
+
if (usingFree) {
|
|
49548
|
+
budgetDowngradeNotice = `> **\u26A0\uFE0F Budget cap reached** ($${runCostUsd.toFixed(4)}/$${options.maxCostPerRunUsd}) \u2014 downgraded to free model. Quality may be reduced. Increase \`maxCostPerRun\` to continue with ${fromModel}.
|
|
49549
|
+
|
|
49550
|
+
`;
|
|
49551
|
+
} else {
|
|
49552
|
+
const toModel = modelsToTry[0] ?? FREE_MODEL;
|
|
49553
|
+
budgetDowngradeNotice = `> **\u26A0\uFE0F Budget low** ($${remainingUsd > 0 ? remainingUsd.toFixed(4) : "0.0000"} remaining) \u2014 using ${toModel} instead of ${fromModel}.
|
|
49554
|
+
|
|
49555
|
+
`;
|
|
49556
|
+
}
|
|
49557
|
+
budgetDowngradeHeaderMode = "downgraded";
|
|
49558
|
+
}
|
|
49559
|
+
}
|
|
49259
49560
|
let upstream;
|
|
49260
49561
|
let lastError;
|
|
49261
49562
|
let actualModelUsed = modelId;
|
|
49262
49563
|
for (let i = 0; i < modelsToTry.length; i++) {
|
|
49263
49564
|
const tryModel = modelsToTry[i];
|
|
49264
49565
|
const isLastAttempt = i === modelsToTry.length - 1;
|
|
49566
|
+
if (globalController.signal.aborted) {
|
|
49567
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
49568
|
+
}
|
|
49265
49569
|
console.log(`[ClawRouter] Trying model ${i + 1}/${modelsToTry.length}: ${tryModel}`);
|
|
49570
|
+
const modelController = new AbortController();
|
|
49571
|
+
const modelTimeoutId = setTimeout(() => modelController.abort(), PER_MODEL_TIMEOUT_MS);
|
|
49572
|
+
const combinedSignal = AbortSignal.any([globalController.signal, modelController.signal]);
|
|
49266
49573
|
const result = await tryModelRequest(
|
|
49267
49574
|
upstreamUrl,
|
|
49268
49575
|
req.method ?? "POST",
|
|
@@ -49272,12 +49579,29 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49272
49579
|
maxTokens,
|
|
49273
49580
|
payFetch,
|
|
49274
49581
|
balanceMonitor,
|
|
49275
|
-
|
|
49582
|
+
combinedSignal
|
|
49276
49583
|
);
|
|
49584
|
+
clearTimeout(modelTimeoutId);
|
|
49585
|
+
if (globalController.signal.aborted) {
|
|
49586
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
|
49587
|
+
}
|
|
49588
|
+
if (!result.success && modelController.signal.aborted && !isLastAttempt) {
|
|
49589
|
+
console.log(
|
|
49590
|
+
`[ClawRouter] Model ${tryModel} timed out after ${PER_MODEL_TIMEOUT_MS}ms, trying fallback`
|
|
49591
|
+
);
|
|
49592
|
+
recordProviderError(tryModel, "server_error");
|
|
49593
|
+
continue;
|
|
49594
|
+
}
|
|
49277
49595
|
if (result.success && result.response) {
|
|
49278
49596
|
upstream = result.response;
|
|
49279
49597
|
actualModelUsed = tryModel;
|
|
49280
49598
|
console.log(`[ClawRouter] Success with model: ${tryModel}`);
|
|
49599
|
+
if (options.maxCostPerRunUsd && effectiveSessionId && tryModel !== FREE_MODEL) {
|
|
49600
|
+
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
49601
|
+
if (costEst) {
|
|
49602
|
+
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
49603
|
+
}
|
|
49604
|
+
}
|
|
49281
49605
|
break;
|
|
49282
49606
|
}
|
|
49283
49607
|
lastError = {
|
|
@@ -49293,7 +49617,52 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49293
49617
|
);
|
|
49294
49618
|
break;
|
|
49295
49619
|
}
|
|
49296
|
-
|
|
49620
|
+
const errorCat = result.errorCategory;
|
|
49621
|
+
if (errorCat) {
|
|
49622
|
+
recordProviderError(tryModel, errorCat);
|
|
49623
|
+
}
|
|
49624
|
+
if (errorCat === "rate_limited") {
|
|
49625
|
+
if (!isLastAttempt && !globalController.signal.aborted) {
|
|
49626
|
+
console.log(
|
|
49627
|
+
`[ClawRouter] Rate-limited on ${tryModel}, retrying in 200ms before failover`
|
|
49628
|
+
);
|
|
49629
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
49630
|
+
if (!globalController.signal.aborted) {
|
|
49631
|
+
const retryController = new AbortController();
|
|
49632
|
+
const retryTimeoutId = setTimeout(
|
|
49633
|
+
() => retryController.abort(),
|
|
49634
|
+
PER_MODEL_TIMEOUT_MS
|
|
49635
|
+
);
|
|
49636
|
+
const retrySignal = AbortSignal.any([
|
|
49637
|
+
globalController.signal,
|
|
49638
|
+
retryController.signal
|
|
49639
|
+
]);
|
|
49640
|
+
const retryResult = await tryModelRequest(
|
|
49641
|
+
upstreamUrl,
|
|
49642
|
+
req.method ?? "POST",
|
|
49643
|
+
headers,
|
|
49644
|
+
body,
|
|
49645
|
+
tryModel,
|
|
49646
|
+
maxTokens,
|
|
49647
|
+
payFetch,
|
|
49648
|
+
balanceMonitor,
|
|
49649
|
+
retrySignal
|
|
49650
|
+
);
|
|
49651
|
+
clearTimeout(retryTimeoutId);
|
|
49652
|
+
if (retryResult.success && retryResult.response) {
|
|
49653
|
+
upstream = retryResult.response;
|
|
49654
|
+
actualModelUsed = tryModel;
|
|
49655
|
+
console.log(`[ClawRouter] Rate-limit retry succeeded for: ${tryModel}`);
|
|
49656
|
+
if (options.maxCostPerRunUsd && effectiveSessionId && tryModel !== FREE_MODEL) {
|
|
49657
|
+
const costEst = estimateAmount(tryModel, body.length, maxTokens);
|
|
49658
|
+
if (costEst) {
|
|
49659
|
+
sessionStore.addSessionCost(effectiveSessionId, BigInt(costEst));
|
|
49660
|
+
}
|
|
49661
|
+
}
|
|
49662
|
+
break;
|
|
49663
|
+
}
|
|
49664
|
+
}
|
|
49665
|
+
}
|
|
49297
49666
|
markRateLimited(tryModel);
|
|
49298
49667
|
try {
|
|
49299
49668
|
const parsed = JSON.parse(result.errorBody || "{}");
|
|
@@ -49309,6 +49678,12 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49309
49678
|
}
|
|
49310
49679
|
} catch {
|
|
49311
49680
|
}
|
|
49681
|
+
} else if (errorCat === "overloaded") {
|
|
49682
|
+
markOverloaded(tryModel);
|
|
49683
|
+
} else if (errorCat === "auth_failure" || errorCat === "quota_exceeded") {
|
|
49684
|
+
console.log(
|
|
49685
|
+
`[ClawRouter] \u{1F511} ${errorCat === "auth_failure" ? "Auth failure" : "Quota exceeded"} for ${tryModel} \u2014 check provider config`
|
|
49686
|
+
);
|
|
49312
49687
|
}
|
|
49313
49688
|
const isPaymentErr = /payment.*verification.*failed|payment.*settlement.*failed|insufficient.*funds|transaction_simulation_failed/i.test(
|
|
49314
49689
|
result.errorBody || ""
|
|
@@ -49423,6 +49798,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49423
49798
|
if (rsp.usage && typeof rsp.usage === "object") {
|
|
49424
49799
|
const u = rsp.usage;
|
|
49425
49800
|
if (typeof u.prompt_tokens === "number") responseInputTokens = u.prompt_tokens;
|
|
49801
|
+
if (typeof u.completion_tokens === "number") responseOutputTokens = u.completion_tokens;
|
|
49426
49802
|
}
|
|
49427
49803
|
const baseChunk = {
|
|
49428
49804
|
id: rsp.id ?? `chatcmpl-${Date.now()}`,
|
|
@@ -49468,6 +49844,25 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49468
49844
|
responseChunks.push(Buffer.from(noticeData));
|
|
49469
49845
|
balanceFallbackNotice = void 0;
|
|
49470
49846
|
}
|
|
49847
|
+
if (budgetDowngradeNotice) {
|
|
49848
|
+
const noticeChunk = {
|
|
49849
|
+
...baseChunk,
|
|
49850
|
+
choices: [
|
|
49851
|
+
{
|
|
49852
|
+
index: index2,
|
|
49853
|
+
delta: { content: budgetDowngradeNotice },
|
|
49854
|
+
logprobs: null,
|
|
49855
|
+
finish_reason: null
|
|
49856
|
+
}
|
|
49857
|
+
]
|
|
49858
|
+
};
|
|
49859
|
+
const noticeData = `data: ${JSON.stringify(noticeChunk)}
|
|
49860
|
+
|
|
49861
|
+
`;
|
|
49862
|
+
safeWrite(res, noticeData);
|
|
49863
|
+
responseChunks.push(Buffer.from(noticeData));
|
|
49864
|
+
budgetDowngradeNotice = void 0;
|
|
49865
|
+
}
|
|
49471
49866
|
if (content) {
|
|
49472
49867
|
const contentChunk = {
|
|
49473
49868
|
...baseChunk,
|
|
@@ -49571,6 +49966,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49571
49966
|
}
|
|
49572
49967
|
balanceFallbackNotice = void 0;
|
|
49573
49968
|
}
|
|
49969
|
+
if (budgetDowngradeNotice && responseBody.length > 0) {
|
|
49970
|
+
try {
|
|
49971
|
+
const parsed = JSON.parse(responseBody.toString());
|
|
49972
|
+
if (parsed.choices?.[0]?.message?.content !== void 0) {
|
|
49973
|
+
parsed.choices[0].message.content = budgetDowngradeNotice + parsed.choices[0].message.content;
|
|
49974
|
+
responseBody = Buffer.from(JSON.stringify(parsed));
|
|
49975
|
+
}
|
|
49976
|
+
} catch {
|
|
49977
|
+
}
|
|
49978
|
+
budgetDowngradeNotice = void 0;
|
|
49979
|
+
}
|
|
49980
|
+
if (budgetDowngradeHeaderMode) {
|
|
49981
|
+
responseHeaders["x-clawrouter-budget-downgrade"] = "1";
|
|
49982
|
+
responseHeaders["x-clawrouter-budget-mode"] = budgetDowngradeHeaderMode;
|
|
49983
|
+
budgetDowngradeHeaderMode = void 0;
|
|
49984
|
+
}
|
|
49574
49985
|
responseHeaders["content-length"] = String(responseBody.length);
|
|
49575
49986
|
res.writeHead(upstream.status, responseHeaders);
|
|
49576
49987
|
safeWrite(res, responseBody);
|
|
@@ -49601,6 +50012,8 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49601
50012
|
if (rspJson.usage && typeof rspJson.usage === "object") {
|
|
49602
50013
|
if (typeof rspJson.usage.prompt_tokens === "number")
|
|
49603
50014
|
responseInputTokens = rspJson.usage.prompt_tokens;
|
|
50015
|
+
if (typeof rspJson.usage.completion_tokens === "number")
|
|
50016
|
+
responseOutputTokens = rspJson.usage.completion_tokens;
|
|
49604
50017
|
}
|
|
49605
50018
|
} catch {
|
|
49606
50019
|
}
|
|
@@ -49633,25 +50046,25 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49633
50046
|
}
|
|
49634
50047
|
const logModel = routingDecision?.model ?? modelId;
|
|
49635
50048
|
if (logModel) {
|
|
49636
|
-
const
|
|
50049
|
+
const actualInputTokens = responseInputTokens ?? Math.ceil(body.length / 4);
|
|
50050
|
+
const actualOutputTokens = responseOutputTokens ?? maxTokens;
|
|
49637
50051
|
const accurateCosts = calculateModelCost(
|
|
49638
50052
|
logModel,
|
|
49639
50053
|
routerOpts.modelPricing,
|
|
49640
|
-
|
|
49641
|
-
|
|
50054
|
+
actualInputTokens,
|
|
50055
|
+
actualOutputTokens,
|
|
49642
50056
|
routingProfile ?? void 0
|
|
49643
50057
|
);
|
|
49644
|
-
const costWithBuffer = accurateCosts.costEstimate * 1.2;
|
|
49645
|
-
const baselineWithBuffer = accurateCosts.baselineCost * 1.2;
|
|
49646
50058
|
const entry = {
|
|
49647
50059
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
49648
50060
|
model: logModel,
|
|
49649
50061
|
tier: routingDecision?.tier ?? "DIRECT",
|
|
49650
|
-
cost:
|
|
49651
|
-
baselineCost:
|
|
50062
|
+
cost: accurateCosts.costEstimate,
|
|
50063
|
+
baselineCost: accurateCosts.baselineCost,
|
|
49652
50064
|
savings: accurateCosts.savings,
|
|
49653
50065
|
latencyMs: Date.now() - startTime,
|
|
49654
|
-
...responseInputTokens !== void 0 && { inputTokens: responseInputTokens }
|
|
50066
|
+
...responseInputTokens !== void 0 && { inputTokens: responseInputTokens },
|
|
50067
|
+
...responseOutputTokens !== void 0 && { outputTokens: responseOutputTokens }
|
|
49655
50068
|
};
|
|
49656
50069
|
logUsage(entry).catch(() => {
|
|
49657
50070
|
});
|
|
@@ -49660,15 +50073,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
49660
50073
|
|
|
49661
50074
|
// src/index.ts
|
|
49662
50075
|
import {
|
|
49663
|
-
writeFileSync as
|
|
50076
|
+
writeFileSync as writeFileSync3,
|
|
49664
50077
|
existsSync as existsSync3,
|
|
49665
50078
|
readdirSync,
|
|
49666
|
-
mkdirSync as
|
|
50079
|
+
mkdirSync as mkdirSync3,
|
|
49667
50080
|
copyFileSync,
|
|
49668
50081
|
renameSync
|
|
49669
50082
|
} from "fs";
|
|
49670
|
-
import { homedir as
|
|
49671
|
-
import { join as
|
|
50083
|
+
import { homedir as homedir7 } from "os";
|
|
50084
|
+
import { join as join10 } from "path";
|
|
49672
50085
|
init_accounts();
|
|
49673
50086
|
|
|
49674
50087
|
// src/partners/registry.ts
|
|
@@ -49772,8 +50185,8 @@ init_solana_balance();
|
|
|
49772
50185
|
// src/spend-control.ts
|
|
49773
50186
|
import * as fs from "fs";
|
|
49774
50187
|
import * as path from "path";
|
|
49775
|
-
import { homedir as
|
|
49776
|
-
var WALLET_DIR2 = path.join(
|
|
50188
|
+
import { homedir as homedir6 } from "os";
|
|
50189
|
+
var WALLET_DIR2 = path.join(homedir6(), ".openclaw", "blockrun");
|
|
49777
50190
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
49778
50191
|
var DAY_MS = 24 * HOUR_MS;
|
|
49779
50192
|
var FileSpendControlStorage = class {
|
|
@@ -50098,13 +50511,13 @@ function isGatewayMode() {
|
|
|
50098
50511
|
return args.includes("gateway");
|
|
50099
50512
|
}
|
|
50100
50513
|
function injectModelsConfig(logger) {
|
|
50101
|
-
const configDir =
|
|
50102
|
-
const configPath =
|
|
50514
|
+
const configDir = join10(homedir7(), ".openclaw");
|
|
50515
|
+
const configPath = join10(configDir, "openclaw.json");
|
|
50103
50516
|
let config = {};
|
|
50104
50517
|
let needsWrite = false;
|
|
50105
50518
|
if (!existsSync3(configDir)) {
|
|
50106
50519
|
try {
|
|
50107
|
-
|
|
50520
|
+
mkdirSync3(configDir, { recursive: true });
|
|
50108
50521
|
logger.info("Created OpenClaw config directory");
|
|
50109
50522
|
} catch (err) {
|
|
50110
50523
|
logger.info(
|
|
@@ -50238,6 +50651,18 @@ function injectModelsConfig(logger) {
|
|
|
50238
50651
|
needsWrite = true;
|
|
50239
50652
|
}
|
|
50240
50653
|
const allowlist = defaults.models;
|
|
50654
|
+
const DEPRECATED_BLOCKRUN_MODELS = ["blockrun/xai/grok-code-fast-1"];
|
|
50655
|
+
let removedDeprecatedCount = 0;
|
|
50656
|
+
for (const key of DEPRECATED_BLOCKRUN_MODELS) {
|
|
50657
|
+
if (allowlist[key]) {
|
|
50658
|
+
delete allowlist[key];
|
|
50659
|
+
removedDeprecatedCount++;
|
|
50660
|
+
}
|
|
50661
|
+
}
|
|
50662
|
+
if (removedDeprecatedCount > 0) {
|
|
50663
|
+
needsWrite = true;
|
|
50664
|
+
logger.info(`Removed ${removedDeprecatedCount} deprecated model entries from allowlist`);
|
|
50665
|
+
}
|
|
50241
50666
|
let addedCount = 0;
|
|
50242
50667
|
for (const id of TOP_MODELS) {
|
|
50243
50668
|
const key = `blockrun/${id}`;
|
|
@@ -50253,7 +50678,7 @@ function injectModelsConfig(logger) {
|
|
|
50253
50678
|
if (needsWrite) {
|
|
50254
50679
|
try {
|
|
50255
50680
|
const tmpPath = `${configPath}.tmp.${process.pid}`;
|
|
50256
|
-
|
|
50681
|
+
writeFileSync3(tmpPath, JSON.stringify(config, null, 2));
|
|
50257
50682
|
renameSync(tmpPath, configPath);
|
|
50258
50683
|
logger.info("Smart routing enabled (blockrun/auto)");
|
|
50259
50684
|
} catch (err) {
|
|
@@ -50262,10 +50687,10 @@ function injectModelsConfig(logger) {
|
|
|
50262
50687
|
}
|
|
50263
50688
|
}
|
|
50264
50689
|
function injectAuthProfile(logger) {
|
|
50265
|
-
const agentsDir =
|
|
50690
|
+
const agentsDir = join10(homedir7(), ".openclaw", "agents");
|
|
50266
50691
|
if (!existsSync3(agentsDir)) {
|
|
50267
50692
|
try {
|
|
50268
|
-
|
|
50693
|
+
mkdirSync3(agentsDir, { recursive: true });
|
|
50269
50694
|
} catch (err) {
|
|
50270
50695
|
logger.info(
|
|
50271
50696
|
`Could not create agents dir: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -50279,11 +50704,11 @@ function injectAuthProfile(logger) {
|
|
|
50279
50704
|
agents = ["main", ...agents];
|
|
50280
50705
|
}
|
|
50281
50706
|
for (const agentId of agents) {
|
|
50282
|
-
const authDir =
|
|
50283
|
-
const authPath =
|
|
50707
|
+
const authDir = join10(agentsDir, agentId, "agent");
|
|
50708
|
+
const authPath = join10(authDir, "auth-profiles.json");
|
|
50284
50709
|
if (!existsSync3(authDir)) {
|
|
50285
50710
|
try {
|
|
50286
|
-
|
|
50711
|
+
mkdirSync3(authDir, { recursive: true });
|
|
50287
50712
|
} catch {
|
|
50288
50713
|
continue;
|
|
50289
50714
|
}
|
|
@@ -50311,7 +50736,7 @@ function injectAuthProfile(logger) {
|
|
|
50311
50736
|
key: "x402-proxy-handles-auth"
|
|
50312
50737
|
};
|
|
50313
50738
|
try {
|
|
50314
|
-
|
|
50739
|
+
writeFileSync3(authPath, JSON.stringify(store, null, 2));
|
|
50315
50740
|
logger.info(`Injected BlockRun auth profile for agent: ${agentId}`);
|
|
50316
50741
|
} catch (err) {
|
|
50317
50742
|
logger.info(
|
|
@@ -50339,9 +50764,18 @@ async function startProxyInBackground(api) {
|
|
|
50339
50764
|
api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${wallet.address}`);
|
|
50340
50765
|
}
|
|
50341
50766
|
const routingConfig = api.pluginConfig?.routing;
|
|
50767
|
+
const maxCostPerRunUsd = typeof api.pluginConfig?.maxCostPerRun === "number" ? api.pluginConfig.maxCostPerRun : void 0;
|
|
50768
|
+
const maxCostPerRunMode = api.pluginConfig?.maxCostPerRunMode === "strict" ? "strict" : "graceful";
|
|
50769
|
+
if (maxCostPerRunUsd !== void 0) {
|
|
50770
|
+
api.logger.info(
|
|
50771
|
+
`Cost cap: $${maxCostPerRunUsd.toFixed(2)} per session (mode: ${maxCostPerRunMode})`
|
|
50772
|
+
);
|
|
50773
|
+
}
|
|
50342
50774
|
const proxy = await startProxy({
|
|
50343
50775
|
wallet,
|
|
50344
50776
|
routingConfig,
|
|
50777
|
+
maxCostPerRunUsd,
|
|
50778
|
+
maxCostPerRunMode,
|
|
50345
50779
|
onReady: (port) => {
|
|
50346
50780
|
api.logger.info(`BlockRun x402 proxy listening on port ${port}`);
|
|
50347
50781
|
},
|
|
@@ -50366,6 +50800,10 @@ async function startProxyInBackground(api) {
|
|
|
50366
50800
|
});
|
|
50367
50801
|
setActiveProxy(proxy);
|
|
50368
50802
|
activeProxyHandle = proxy;
|
|
50803
|
+
const startupExclusions = loadExcludeList();
|
|
50804
|
+
if (startupExclusions.size > 0) {
|
|
50805
|
+
api.logger.info(`Model exclusions active (${startupExclusions.size}): ${[...startupExclusions].join(", ")}`);
|
|
50806
|
+
}
|
|
50369
50807
|
api.logger.info(`ClawRouter ready \u2014 smart routing enabled`);
|
|
50370
50808
|
api.logger.info(`Pricing: Simple ~$0.001 | Code ~$0.01 | Complex ~$0.05 | Free: $0`);
|
|
50371
50809
|
const currentChain = await resolvePaymentChain();
|
|
@@ -50425,6 +50863,78 @@ async function createStatsCommand() {
|
|
|
50425
50863
|
}
|
|
50426
50864
|
};
|
|
50427
50865
|
}
|
|
50866
|
+
async function createExcludeCommand() {
|
|
50867
|
+
return {
|
|
50868
|
+
name: "exclude",
|
|
50869
|
+
description: "Manage excluded models \u2014 /exclude add|remove|clear <model>",
|
|
50870
|
+
acceptsArgs: true,
|
|
50871
|
+
requireAuth: true,
|
|
50872
|
+
handler: async (ctx) => {
|
|
50873
|
+
const args = ctx.args?.trim() || "";
|
|
50874
|
+
const parts = args.split(/\s+/);
|
|
50875
|
+
const subcommand = parts[0]?.toLowerCase() || "";
|
|
50876
|
+
const modelArg = parts.slice(1).join(" ").trim();
|
|
50877
|
+
if (!subcommand) {
|
|
50878
|
+
const list = loadExcludeList();
|
|
50879
|
+
if (list.size === 0) {
|
|
50880
|
+
return {
|
|
50881
|
+
text: "No models excluded.\n\nUsage:\n /exclude add <model> \u2014 block a model\n /exclude remove <model> \u2014 unblock\n /exclude clear \u2014 remove all"
|
|
50882
|
+
};
|
|
50883
|
+
}
|
|
50884
|
+
const models = [...list].sort().map((m) => ` \u2022 ${m}`).join("\n");
|
|
50885
|
+
return {
|
|
50886
|
+
text: `Excluded models (${list.size}):
|
|
50887
|
+
${models}
|
|
50888
|
+
|
|
50889
|
+
Use /exclude remove <model> to unblock.`
|
|
50890
|
+
};
|
|
50891
|
+
}
|
|
50892
|
+
if (subcommand === "add") {
|
|
50893
|
+
if (!modelArg) {
|
|
50894
|
+
return { text: "Usage: /exclude add <model>\nExample: /exclude add nvidia/gpt-oss-120b", isError: true };
|
|
50895
|
+
}
|
|
50896
|
+
const resolved = addExclusion(modelArg);
|
|
50897
|
+
const list = loadExcludeList();
|
|
50898
|
+
return {
|
|
50899
|
+
text: `Excluded: ${resolved}
|
|
50900
|
+
|
|
50901
|
+
Active exclusions (${list.size}):
|
|
50902
|
+
${[...list].sort().map((m) => ` \u2022 ${m}`).join("\n")}`
|
|
50903
|
+
};
|
|
50904
|
+
}
|
|
50905
|
+
if (subcommand === "remove") {
|
|
50906
|
+
if (!modelArg) {
|
|
50907
|
+
return { text: "Usage: /exclude remove <model>", isError: true };
|
|
50908
|
+
}
|
|
50909
|
+
const removed = removeExclusion(modelArg);
|
|
50910
|
+
if (!removed) {
|
|
50911
|
+
return { text: `Model "${modelArg}" was not in the exclude list.` };
|
|
50912
|
+
}
|
|
50913
|
+
const list = loadExcludeList();
|
|
50914
|
+
return {
|
|
50915
|
+
text: `Unblocked: ${modelArg}
|
|
50916
|
+
|
|
50917
|
+
Active exclusions (${list.size}):
|
|
50918
|
+
${list.size > 0 ? [...list].sort().map((m) => ` \u2022 ${m}`).join("\n") : " (none)"}`
|
|
50919
|
+
};
|
|
50920
|
+
}
|
|
50921
|
+
if (subcommand === "clear") {
|
|
50922
|
+
clearExclusions();
|
|
50923
|
+
return { text: "All model exclusions cleared." };
|
|
50924
|
+
}
|
|
50925
|
+
return {
|
|
50926
|
+
text: `Unknown subcommand: ${subcommand}
|
|
50927
|
+
|
|
50928
|
+
Usage:
|
|
50929
|
+
/exclude \u2014 show list
|
|
50930
|
+
/exclude add <model>
|
|
50931
|
+
/exclude remove <model>
|
|
50932
|
+
/exclude clear`,
|
|
50933
|
+
isError: true
|
|
50934
|
+
};
|
|
50935
|
+
}
|
|
50936
|
+
};
|
|
50937
|
+
}
|
|
50428
50938
|
async function createWalletCommand() {
|
|
50429
50939
|
return {
|
|
50430
50940
|
name: "wallet",
|
|
@@ -50737,6 +51247,13 @@ var plugin = {
|
|
|
50737
51247
|
`Failed to register /stats command: ${err instanceof Error ? err.message : String(err)}`
|
|
50738
51248
|
);
|
|
50739
51249
|
});
|
|
51250
|
+
createExcludeCommand().then((excludeCommand) => {
|
|
51251
|
+
api.registerCommand(excludeCommand);
|
|
51252
|
+
}).catch((err) => {
|
|
51253
|
+
api.logger.warn(
|
|
51254
|
+
`Failed to register /exclude command: ${err instanceof Error ? err.message : String(err)}`
|
|
51255
|
+
);
|
|
51256
|
+
});
|
|
50740
51257
|
api.registerService({
|
|
50741
51258
|
id: "clawrouter-proxy",
|
|
50742
51259
|
start: () => {
|