@blockrun/clawrouter 0.10.21 → 0.11.0

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/index.d.ts CHANGED
@@ -477,6 +477,9 @@ type SessionEntry = {
477
477
  createdAt: number;
478
478
  lastUsedAt: number;
479
479
  requestCount: number;
480
+ recentHashes: string[];
481
+ strikes: number;
482
+ escalated: boolean;
480
483
  };
481
484
  type SessionConfig = {
482
485
  /** Enable session persistence (default: false) */
@@ -530,6 +533,21 @@ declare class SessionStore {
530
533
  * Clean up expired sessions.
531
534
  */
532
535
  private cleanup;
536
+ /**
537
+ * Record a request content hash and detect repetitive patterns.
538
+ * Returns true if escalation should be triggered (3+ consecutive similar requests).
539
+ */
540
+ recordRequestHash(sessionId: string, hash: string): boolean;
541
+ /**
542
+ * Escalate session to next tier. Returns the new model/tier or null if already at max.
543
+ */
544
+ escalateSession(sessionId: string, tierConfigs: Record<string, {
545
+ primary: string;
546
+ fallback: string[];
547
+ }>): {
548
+ model: string;
549
+ tier: string;
550
+ } | null;
533
551
  /**
534
552
  * Stop the cleanup interval.
535
553
  */
@@ -539,6 +557,12 @@ declare class SessionStore {
539
557
  * Generate a session ID from request headers or create a default.
540
558
  */
541
559
  declare function getSessionId(headers: Record<string, string | string[] | undefined>, headerName?: string): string | undefined;
560
+ /**
561
+ * Generate a short hash fingerprint from request content.
562
+ * Captures: last user message text + tool call names (if any).
563
+ * Normalizes whitespace to avoid false negatives from minor formatting diffs.
564
+ */
565
+ declare function hashRequestContent(lastUserContent: string, toolCallNames?: string[]): string;
542
566
 
543
567
  /**
544
568
  * Local x402 Proxy Server
@@ -1099,4 +1123,4 @@ declare function buildPartnerTools(proxyBaseUrl: string): PartnerToolDefinition[
1099
1123
 
1100
1124
  declare const plugin: OpenClawPluginDefinition;
1101
1125
 
1102
- export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, type CachedLLMResponse, type CachedPaymentParams, type CachedResponse, DEFAULT_RETRY_CONFIG, DEFAULT_ROUTING_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, EmptyWalletError, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, PaymentCache, type PaymentFetchResult, type PreAuthParams, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, type RoutingConfig, type RoutingDecision, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SufficiencyResult, type Tier, type UsageEntry, blockrunProvider, buildPartnerTools, buildProviderModels, calculateModelCost, createPaymentFetch, plugin as default, fetchWithRetry, formatStatsAscii, getAgenticModels, getFallbackChain, getFallbackChainFiltered, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, isAgenticModel, isBalanceError, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, logUsage, resolveModelAlias, route, startProxy };
1126
+ export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, type CachedLLMResponse, type CachedPaymentParams, type CachedResponse, DEFAULT_RETRY_CONFIG, DEFAULT_ROUTING_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, EmptyWalletError, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, PaymentCache, type PaymentFetchResult, type PreAuthParams, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, type RoutingConfig, type RoutingDecision, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SufficiencyResult, type Tier, type UsageEntry, blockrunProvider, buildPartnerTools, buildProviderModels, calculateModelCost, createPaymentFetch, plugin as default, fetchWithRetry, formatStatsAscii, getAgenticModels, getFallbackChain, getFallbackChainFiltered, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, isAgenticModel, isBalanceError, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, logUsage, resolveModelAlias, route, startProxy };
package/dist/index.js CHANGED
@@ -42,6 +42,8 @@ var MODEL_ALIASES = {
42
42
  // Google
43
43
  gemini: "google/gemini-2.5-pro",
44
44
  flash: "google/gemini-2.5-flash",
45
+ "gemini-3.1-pro-preview": "google/gemini-3.1-pro",
46
+ "google/gemini-3.1-pro-preview": "google/gemini-3.1-pro",
45
47
  // xAI
46
48
  grok: "xai/grok-3",
47
49
  "grok-fast": "xai/grok-4-fast-reasoning",
@@ -281,6 +283,7 @@ var BLOCKRUN_MODELS = [
281
283
  outputPrice: 5,
282
284
  contextWindow: 2e5,
283
285
  maxOutput: 8192,
286
+ vision: true,
284
287
  agentic: true,
285
288
  toolCalling: true
286
289
  },
@@ -293,6 +296,7 @@ var BLOCKRUN_MODELS = [
293
296
  contextWindow: 2e5,
294
297
  maxOutput: 64e3,
295
298
  reasoning: true,
299
+ vision: true,
296
300
  agentic: true,
297
301
  toolCalling: true
298
302
  },
@@ -305,6 +309,7 @@ var BLOCKRUN_MODELS = [
305
309
  contextWindow: 2e5,
306
310
  maxOutput: 32e3,
307
311
  reasoning: true,
312
+ vision: true,
308
313
  agentic: true,
309
314
  toolCalling: true
310
315
  },
@@ -364,6 +369,7 @@ var BLOCKRUN_MODELS = [
364
369
  outputPrice: 2.5,
365
370
  contextWindow: 1e6,
366
371
  maxOutput: 65536,
372
+ vision: true,
367
373
  toolCalling: true
368
374
  },
369
375
  {
@@ -595,6 +601,11 @@ function supportsToolCalling(modelId) {
595
601
  const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
596
602
  return model?.toolCalling ?? false;
597
603
  }
604
+ function supportsVision(modelId) {
605
+ const normalized = modelId.replace("blockrun/", "");
606
+ const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
607
+ return model?.vision ?? false;
608
+ }
598
609
  function getModelContextWindow(modelId) {
599
610
  const normalized = modelId.replace("blockrun/", "");
600
611
  const model = BLOCKRUN_MODELS.find((m) => m.id === normalized);
@@ -1196,6 +1207,11 @@ function filterByToolCalling(models, hasTools, supportsToolCalling2) {
1196
1207
  const filtered = models.filter(supportsToolCalling2);
1197
1208
  return filtered.length > 0 ? filtered : models;
1198
1209
  }
1210
+ function filterByVision(models, hasVision, supportsVision2) {
1211
+ if (!hasVision) return models;
1212
+ const filtered = models.filter(supportsVision2);
1213
+ return filtered.length > 0 ? filtered : models;
1214
+ }
1199
1215
  function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getContextWindow) {
1200
1216
  const fullChain = getFallbackChain(tier, tierConfigs);
1201
1217
  const filtered = fullChain.filter((modelId) => {
@@ -4039,7 +4055,10 @@ var SessionStore = class {
4039
4055
  tier,
4040
4056
  createdAt: now,
4041
4057
  lastUsedAt: now,
4042
- requestCount: 1
4058
+ requestCount: 1,
4059
+ recentHashes: [],
4060
+ strikes: 0,
4061
+ escalated: false
4043
4062
  });
4044
4063
  }
4045
4064
  }
@@ -4091,6 +4110,43 @@ var SessionStore = class {
4091
4110
  }
4092
4111
  }
4093
4112
  }
4113
+ /**
4114
+ * Record a request content hash and detect repetitive patterns.
4115
+ * Returns true if escalation should be triggered (3+ consecutive similar requests).
4116
+ */
4117
+ recordRequestHash(sessionId, hash) {
4118
+ const entry = this.sessions.get(sessionId);
4119
+ if (!entry) return false;
4120
+ const prev = entry.recentHashes;
4121
+ if (prev.length > 0 && prev[prev.length - 1] === hash) {
4122
+ entry.strikes++;
4123
+ } else {
4124
+ entry.strikes = 0;
4125
+ }
4126
+ entry.recentHashes.push(hash);
4127
+ if (entry.recentHashes.length > 3) {
4128
+ entry.recentHashes.shift();
4129
+ }
4130
+ return entry.strikes >= 2 && !entry.escalated;
4131
+ }
4132
+ /**
4133
+ * Escalate session to next tier. Returns the new model/tier or null if already at max.
4134
+ */
4135
+ escalateSession(sessionId, tierConfigs) {
4136
+ const entry = this.sessions.get(sessionId);
4137
+ if (!entry) return null;
4138
+ const TIER_ORDER = ["SIMPLE", "MEDIUM", "COMPLEX", "REASONING"];
4139
+ const currentIdx = TIER_ORDER.indexOf(entry.tier);
4140
+ if (currentIdx < 0 || currentIdx >= TIER_ORDER.length - 1) return null;
4141
+ const nextTier = TIER_ORDER[currentIdx + 1];
4142
+ const nextConfig = tierConfigs[nextTier];
4143
+ if (!nextConfig) return null;
4144
+ entry.model = nextConfig.primary;
4145
+ entry.tier = nextTier;
4146
+ entry.strikes = 0;
4147
+ entry.escalated = true;
4148
+ return { model: nextConfig.primary, tier: nextTier };
4149
+ }
4094
4150
  /**
4095
4151
  * Stop the cleanup interval.
4096
4152
  */
@@ -4117,6 +4173,11 @@ function deriveSessionId(messages) {
4117
4173
  const content = typeof firstUser.content === "string" ? firstUser.content : JSON.stringify(firstUser.content);
4118
4174
  return createHash3("sha256").update(content).digest("hex").slice(0, 8);
4119
4175
  }
4176
+ function hashRequestContent(lastUserContent, toolCallNames) {
4177
+ const normalized = lastUserContent.replace(/\s+/g, " ").trim().slice(0, 500);
4178
+ const toolSuffix = toolCallNames?.length ? `|tools:${toolCallNames.sort().join(",")}` : "";
4179
+ return createHash3("sha256").update(normalized + toolSuffix).digest("hex").slice(0, 12);
4180
+ }
4120
4181
 
4121
4182
  // src/updater.ts
4122
4183
  var NPM_REGISTRY = "https://registry.npmjs.org/@blockrun/clawrouter/latest";
@@ -5247,6 +5308,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5247
5308
  const debugMode = req.headers["x-clawrouter-debug"] !== "false";
5248
5309
  let routingDecision;
5249
5310
  let hasTools = false;
5311
+ let hasVision = false;
5250
5312
  let isStreaming = false;
5251
5313
  let modelId = "";
5252
5314
  let maxTokens = 4096;
@@ -5388,6 +5450,154 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5388
5450
  console.log(`[ClawRouter] /debug command \u2192 ${debugRouting.tier} | ${debugRouting.model}`);
5389
5451
  return;
5390
5452
  }
5453
+ if (lastContent.startsWith("/imagegen")) {
5454
+ const imageArgs = lastContent.slice("/imagegen".length).trim();
5455
+ let imageModel = "google/nano-banana";
5456
+ let imageSize = "1024x1024";
5457
+ let imagePrompt = imageArgs;
5458
+ const modelMatch = imageArgs.match(/--model\s+(\S+)/);
5459
+ if (modelMatch) {
5460
+ const raw = modelMatch[1];
5461
+ const IMAGE_MODEL_ALIASES = {
5462
+ "dall-e-3": "openai/dall-e-3",
5463
+ "dalle3": "openai/dall-e-3",
5464
+ "dalle": "openai/dall-e-3",
5465
+ "gpt-image": "openai/gpt-image-1",
5466
+ "gpt-image-1": "openai/gpt-image-1",
5467
+ "flux": "black-forest/flux-1.1-pro",
5468
+ "flux-pro": "black-forest/flux-1.1-pro",
5469
+ "banana": "google/nano-banana",
5470
+ "nano-banana": "google/nano-banana",
5471
+ "banana-pro": "google/nano-banana-pro",
5472
+ "nano-banana-pro": "google/nano-banana-pro"
5473
+ };
5474
+ imageModel = IMAGE_MODEL_ALIASES[raw] ?? raw;
5475
+ imagePrompt = imagePrompt.replace(/--model\s+\S+/, "").trim();
5476
+ }
5477
+ const sizeMatch = imageArgs.match(/--size\s+(\d+x\d+)/);
5478
+ if (sizeMatch) {
5479
+ imageSize = sizeMatch[1];
5480
+ imagePrompt = imagePrompt.replace(/--size\s+\d+x\d+/, "").trim();
5481
+ }
5482
+ if (!imagePrompt) {
5483
+ const errorText = [
5484
+ "Usage: /imagegen <prompt>",
5485
+ "",
5486
+ "Options:",
5487
+ " --model <model> Model to use (default: nano-banana)",
5488
+ " --size <WxH> Image size (default: 1024x1024)",
5489
+ "",
5490
+ "Models:",
5491
+ " nano-banana Google Gemini Flash \u2014 $0.05/image",
5492
+ " banana-pro Google Gemini Pro \u2014 $0.10/image (up to 4K)",
5493
+ " dall-e-3 OpenAI DALL-E 3 \u2014 $0.04/image",
5494
+ " gpt-image OpenAI GPT Image 1 \u2014 $0.02/image",
5495
+ " flux Black Forest Flux 1.1 Pro \u2014 $0.04/image",
5496
+ "",
5497
+ "Examples:",
5498
+ " /imagegen a cat wearing sunglasses",
5499
+ " /imagegen --model dall-e-3 a futuristic city at sunset",
5500
+ " /imagegen --model banana-pro --size 2048x2048 mountain landscape"
5501
+ ].join("\n");
5502
+ const completionId = `chatcmpl-image-${Date.now()}`;
5503
+ const timestamp = Math.floor(Date.now() / 1e3);
5504
+ if (isStreaming) {
5505
+ res.writeHead(200, {
5506
+ "Content-Type": "text/event-stream",
5507
+ "Cache-Control": "no-cache",
5508
+ Connection: "keep-alive"
5509
+ });
5510
+ res.write(`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: { role: "assistant", content: errorText }, finish_reason: null }] })}
5511
+
5512
+ `);
5513
+ res.write(`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
5514
+
5515
+ `);
5516
+ res.write("data: [DONE]\n\n");
5517
+ res.end();
5518
+ } else {
5519
+ res.writeHead(200, { "Content-Type": "application/json" });
5520
+ res.end(JSON.stringify({
5521
+ id: completionId,
5522
+ object: "chat.completion",
5523
+ created: timestamp,
5524
+ model: "clawrouter/image",
5525
+ choices: [{ index: 0, message: { role: "assistant", content: errorText }, finish_reason: "stop" }],
5526
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
5527
+ }));
5528
+ }
5529
+ console.log(`[ClawRouter] /imagegen command \u2192 showing usage help`);
5530
+ return;
5531
+ }
5532
+ console.log(`[ClawRouter] /imagegen command \u2192 ${imageModel} (${imageSize}): ${imagePrompt.slice(0, 80)}...`);
5533
+ try {
5534
+ const imageUpstreamUrl = `${apiBase}/v1/images/generations`;
5535
+ const imageBody = JSON.stringify({ model: imageModel, prompt: imagePrompt, size: imageSize, n: 1 });
5536
+ const imageResponse = await payFetch(imageUpstreamUrl, {
5537
+ method: "POST",
5538
+ headers: { "content-type": "application/json", "user-agent": USER_AGENT },
5539
+ body: imageBody
5540
+ });
5541
+ const imageResult = await imageResponse.json();
5542
+ let responseText;
5543
+ if (!imageResponse.ok || imageResult.error) {
5544
+ const errMsg = typeof imageResult.error === "string" ? imageResult.error : imageResult.error?.message ?? `HTTP ${imageResponse.status}`;
5545
+ responseText = `Image generation failed: ${errMsg}`;
5546
+ console.log(`[ClawRouter] /imagegen error: ${errMsg}`);
5547
+ } else {
5548
+ const images = imageResult.data ?? [];
5549
+ if (images.length === 0) {
5550
+ responseText = "Image generation returned no results.";
5551
+ } else {
5552
+ const lines = [];
5553
+ for (const img of images) {
5554
+ if (img.url) lines.push(`![Generated Image](${img.url})`);
5555
+ if (img.revised_prompt) lines.push(`*Revised prompt: ${img.revised_prompt}*`);
5556
+ }
5557
+ lines.push("", `Model: ${imageModel} | Size: ${imageSize}`);
5558
+ responseText = lines.join("\n");
5559
+ }
5560
+ console.log(`[ClawRouter] /imagegen success: ${images.length} image(s) generated`);
5561
+ }
5562
+ const completionId = `chatcmpl-image-${Date.now()}`;
5563
+ const timestamp = Math.floor(Date.now() / 1e3);
5564
+ if (isStreaming) {
5565
+ res.writeHead(200, {
5566
+ "Content-Type": "text/event-stream",
5567
+ "Cache-Control": "no-cache",
5568
+ Connection: "keep-alive"
5569
+ });
5570
+ res.write(`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: { role: "assistant", content: responseText }, finish_reason: null }] })}
5571
+
5572
+ `);
5573
+ res.write(`data: ${JSON.stringify({ id: completionId, object: "chat.completion.chunk", created: timestamp, model: "clawrouter/image", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}
5574
+
5575
+ `);
5576
+ res.write("data: [DONE]\n\n");
5577
+ res.end();
5578
+ } else {
5579
+ res.writeHead(200, { "Content-Type": "application/json" });
5580
+ res.end(JSON.stringify({
5581
+ id: completionId,
5582
+ object: "chat.completion",
5583
+ created: timestamp,
5584
+ model: "clawrouter/image",
5585
+ choices: [{ index: 0, message: { role: "assistant", content: responseText }, finish_reason: "stop" }],
5586
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 }
5587
+ }));
5588
+ }
5589
+ } catch (err) {
5590
+ const errMsg = err instanceof Error ? err.message : String(err);
5591
+ console.error(`[ClawRouter] /imagegen error: ${errMsg}`);
5592
+ if (!res.headersSent) {
5593
+ res.writeHead(500, { "Content-Type": "application/json" });
5594
+ res.end(JSON.stringify({
5595
+ error: { message: `Image generation failed: ${errMsg}`, type: "image_error" }
5596
+ }));
5597
+ }
5598
+ }
5599
+ return;
5600
+ }
5391
5601
  if (parsed.stream === true) {
5392
5602
  parsed.stream = false;
5393
5603
  bodyModified = true;
@@ -5439,6 +5649,15 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5439
5649
  if (hasTools && tools) {
5440
5650
  console.log(`[ClawRouter] Tools detected (${tools.length}), agentic mode via keywords`);
5441
5651
  }
5652
+ hasVision = parsedMessages.some((m) => {
5653
+ if (Array.isArray(m.content)) {
5654
+ return m.content.some((p) => p.type === "image_url");
5655
+ }
5656
+ return false;
5657
+ });
5658
+ if (hasVision) {
5659
+ console.log(`[ClawRouter] Vision content detected, filtering to vision-capable models`);
5660
+ }
5442
5661
  routingDecision = route(prompt, systemPrompt, maxTokens, {
5443
5662
  ...routerOpts,
5444
5663
  routingProfile: routingProfile ?? void 0
@@ -5480,6 +5699,43 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5480
5699
  tier: existingSession.tier
5481
5700
  };
5482
5701
  }
5702
+ const lastAssistantMsg = [...parsedMessages].reverse().find((m) => m.role === "assistant");
5703
+ const toolCallNames = Array.isArray(lastAssistantMsg?.tool_calls) ? lastAssistantMsg.tool_calls.map((tc) => tc.function?.name).filter(Boolean) : void 0;
5704
+ const contentHash = hashRequestContent(prompt, toolCallNames);
5705
+ const shouldEscalate = sessionStore.recordRequestHash(
5706
+ effectiveSessionId,
5707
+ contentHash
5708
+ );
5709
+ if (shouldEscalate) {
5710
+ const activeTierConfigs = (() => {
5711
+ if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
5712
+ return routerOpts.config.agenticTiers;
5713
+ }
5714
+ if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
5715
+ return routerOpts.config.ecoTiers;
5716
+ }
5717
+ if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
5718
+ return routerOpts.config.premiumTiers;
5719
+ }
5720
+ return routerOpts.config.tiers;
5721
+ })();
5722
+ const escalation = sessionStore.escalateSession(
5723
+ effectiveSessionId,
5724
+ activeTierConfigs
5725
+ );
5726
+ if (escalation) {
5727
+ console.log(
5728
+ `[ClawRouter] \u26A1 3-strike escalation: ${existingSession.model} \u2192 ${escalation.model} (${existingSession.tier} \u2192 ${escalation.tier})`
5729
+ );
5730
+ parsed.model = escalation.model;
5731
+ modelId = escalation.model;
5732
+ routingDecision = {
5733
+ ...routingDecision,
5734
+ model: escalation.model,
5735
+ tier: escalation.tier
5736
+ };
5737
+ }
5738
+ }
5483
5739
  } else {
5484
5740
  parsed.model = routingDecision.model;
5485
5741
  modelId = routingDecision.model;
@@ -5699,7 +5955,14 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5699
5955
  `[ClawRouter] Tool-calling filter: excluded ${toolExcluded.join(", ")} (no structured function call support)`
5700
5956
  );
5701
5957
  }
5702
- modelsToTry = toolFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
5958
+ const visionFiltered = filterByVision(toolFiltered, hasVision, supportsVision);
5959
+ const visionExcluded = toolFiltered.filter((m) => !visionFiltered.includes(m));
5960
+ if (visionExcluded.length > 0) {
5961
+ console.log(
5962
+ `[ClawRouter] Vision filter: excluded ${visionExcluded.join(", ")} (no vision support)`
5963
+ );
5964
+ }
5965
+ modelsToTry = visionFiltered.slice(0, MAX_FALLBACK_ATTEMPTS);
5703
5966
  modelsToTry = prioritizeNonRateLimited(modelsToTry);
5704
5967
  } else {
5705
5968
  modelsToTry = modelId ? [modelId] : [];
@@ -5745,9 +6008,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
5745
6008
  if (isPaymentErr && tryModel !== FREE_MODEL) {
5746
6009
  const freeIdx = modelsToTry.indexOf(FREE_MODEL);
5747
6010
  if (freeIdx > i + 1) {
5748
- console.log(
5749
- `[ClawRouter] Payment error \u2014 skipping to free model: ${FREE_MODEL}`
5750
- );
6011
+ console.log(`[ClawRouter] Payment error \u2014 skipping to free model: ${FREE_MODEL}`);
5751
6012
  i = freeIdx - 1;
5752
6013
  continue;
5753
6014
  }
@@ -6856,6 +7117,7 @@ export {
6856
7117
  getProxyPort,
6857
7118
  getSessionId,
6858
7119
  getStats,
7120
+ hashRequestContent,
6859
7121
  isAgenticModel,
6860
7122
  isBalanceError,
6861
7123
  isEmptyWalletError,