@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/cli.js +266 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +267 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/update.sh +27 -0
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(``);
|
|
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
|
-
|
|
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,
|