@blockrun/clawrouter 0.9.8 → 0.9.9
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 +6 -4
- package/dist/cli.js +286 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +295 -17
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
package/dist/index.d.ts
CHANGED
|
@@ -281,6 +281,91 @@ type RouterOptions = {
|
|
|
281
281
|
*/
|
|
282
282
|
declare function route(prompt: string, systemPrompt: string | undefined, maxOutputTokens: number, options: RouterOptions): RoutingDecision;
|
|
283
283
|
|
|
284
|
+
/**
|
|
285
|
+
* Response Cache for LLM Completions
|
|
286
|
+
*
|
|
287
|
+
* Caches LLM responses by request hash (model + messages + params).
|
|
288
|
+
* Inspired by LiteLLM's caching system. Returns cached responses for
|
|
289
|
+
* identical requests, saving both cost and latency.
|
|
290
|
+
*
|
|
291
|
+
* Features:
|
|
292
|
+
* - TTL-based expiration (default 10 minutes)
|
|
293
|
+
* - LRU eviction when cache is full
|
|
294
|
+
* - Size limits per item (1MB max)
|
|
295
|
+
* - Heap-based expiration tracking for efficient pruning
|
|
296
|
+
*/
|
|
297
|
+
type CachedLLMResponse = {
|
|
298
|
+
body: Buffer;
|
|
299
|
+
status: number;
|
|
300
|
+
headers: Record<string, string>;
|
|
301
|
+
model: string;
|
|
302
|
+
cachedAt: number;
|
|
303
|
+
expiresAt: number;
|
|
304
|
+
};
|
|
305
|
+
type ResponseCacheConfig = {
|
|
306
|
+
/** Maximum number of cached responses. Default: 200 */
|
|
307
|
+
maxSize?: number;
|
|
308
|
+
/** Default TTL in seconds. Default: 600 (10 minutes) */
|
|
309
|
+
defaultTTL?: number;
|
|
310
|
+
/** Maximum size per cached item in bytes. Default: 1MB */
|
|
311
|
+
maxItemSize?: number;
|
|
312
|
+
/** Enable/disable cache. Default: true */
|
|
313
|
+
enabled?: boolean;
|
|
314
|
+
};
|
|
315
|
+
declare class ResponseCache {
|
|
316
|
+
private cache;
|
|
317
|
+
private expirationHeap;
|
|
318
|
+
private config;
|
|
319
|
+
private stats;
|
|
320
|
+
constructor(config?: ResponseCacheConfig);
|
|
321
|
+
/**
|
|
322
|
+
* Generate cache key from request body.
|
|
323
|
+
* Hashes: model + messages + temperature + max_tokens + other params
|
|
324
|
+
*/
|
|
325
|
+
static generateKey(body: Buffer | string): string;
|
|
326
|
+
/**
|
|
327
|
+
* Check if caching is enabled for this request.
|
|
328
|
+
* Respects cache control headers and request params.
|
|
329
|
+
*/
|
|
330
|
+
shouldCache(body: Buffer | string, headers?: Record<string, string>): boolean;
|
|
331
|
+
/**
|
|
332
|
+
* Get cached response if available and not expired.
|
|
333
|
+
*/
|
|
334
|
+
get(key: string): CachedLLMResponse | undefined;
|
|
335
|
+
/**
|
|
336
|
+
* Cache a response with optional custom TTL.
|
|
337
|
+
*/
|
|
338
|
+
set(key: string, response: {
|
|
339
|
+
body: Buffer;
|
|
340
|
+
status: number;
|
|
341
|
+
headers: Record<string, string>;
|
|
342
|
+
model: string;
|
|
343
|
+
}, ttlSeconds?: number): void;
|
|
344
|
+
/**
|
|
345
|
+
* Evict expired and oldest entries to make room.
|
|
346
|
+
*/
|
|
347
|
+
private evict;
|
|
348
|
+
/**
|
|
349
|
+
* Get cache statistics.
|
|
350
|
+
*/
|
|
351
|
+
getStats(): {
|
|
352
|
+
size: number;
|
|
353
|
+
maxSize: number;
|
|
354
|
+
hits: number;
|
|
355
|
+
misses: number;
|
|
356
|
+
evictions: number;
|
|
357
|
+
hitRate: string;
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Clear all cached entries.
|
|
361
|
+
*/
|
|
362
|
+
clear(): void;
|
|
363
|
+
/**
|
|
364
|
+
* Check if cache is enabled.
|
|
365
|
+
*/
|
|
366
|
+
isEnabled(): boolean;
|
|
367
|
+
}
|
|
368
|
+
|
|
284
369
|
/**
|
|
285
370
|
* Balance Monitor for ClawRouter
|
|
286
371
|
*
|
|
@@ -521,6 +606,12 @@ type ProxyOptions = {
|
|
|
521
606
|
* Set to 0 to compress all requests.
|
|
522
607
|
*/
|
|
523
608
|
compressionThresholdKB?: number;
|
|
609
|
+
/**
|
|
610
|
+
* Response caching config. When enabled, identical requests return
|
|
611
|
+
* cached responses instead of making new API calls.
|
|
612
|
+
* Default: enabled with 10 minute TTL, 200 max entries.
|
|
613
|
+
*/
|
|
614
|
+
cacheConfig?: ResponseCacheConfig;
|
|
524
615
|
onReady?: (port: number) => void;
|
|
525
616
|
onError?: (error: Error) => void;
|
|
526
617
|
onPayment?: (info: {
|
|
@@ -917,4 +1008,4 @@ declare function formatStatsAscii(stats: AggregatedStats): string;
|
|
|
917
1008
|
|
|
918
1009
|
declare const plugin: OpenClawPluginDefinition;
|
|
919
1010
|
|
|
920
|
-
export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, 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, PaymentCache, type PaymentFetchResult, type PreAuthParams, type ProxyHandle, type ProxyOptions, RequestDeduplicator, type RetryConfig, type RoutingConfig, type RoutingDecision, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SufficiencyResult, type Tier, type UsageEntry, blockrunProvider, buildProviderModels, calculateModelCost, createPaymentFetch, plugin as default, fetchWithRetry, formatStatsAscii, getAgenticModels, getFallbackChain, getFallbackChainFiltered, getModelContextWindow, getProxyPort, getSessionId, getStats, isAgenticModel, isBalanceError, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, logUsage, resolveModelAlias, route, startProxy };
|
|
1011
|
+
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, 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, buildProviderModels, calculateModelCost, createPaymentFetch, plugin as default, fetchWithRetry, formatStatsAscii, getAgenticModels, getFallbackChain, getFallbackChainFiltered, getModelContextWindow, getProxyPort, getSessionId, getStats, isAgenticModel, isBalanceError, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, logUsage, resolveModelAlias, route, startProxy };
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,16 @@ var MODEL_ALIASES = {
|
|
|
3
3
|
// Claude
|
|
4
4
|
claude: "anthropic/claude-sonnet-4",
|
|
5
5
|
sonnet: "anthropic/claude-sonnet-4",
|
|
6
|
-
opus: "anthropic/claude-opus-4",
|
|
6
|
+
opus: "anthropic/claude-opus-4.6",
|
|
7
|
+
// Updated to latest Opus 4.6
|
|
8
|
+
"opus-46": "anthropic/claude-opus-4.6",
|
|
9
|
+
"opus-45": "anthropic/claude-opus-4.5",
|
|
7
10
|
haiku: "anthropic/claude-haiku-4.5",
|
|
8
11
|
// OpenAI
|
|
9
12
|
gpt: "openai/gpt-4o",
|
|
10
13
|
gpt4: "openai/gpt-4o",
|
|
11
14
|
gpt5: "openai/gpt-5.2",
|
|
15
|
+
codex: "openai/gpt-5.2-codex",
|
|
12
16
|
mini: "openai/gpt-4o-mini",
|
|
13
17
|
o3: "openai/o3",
|
|
14
18
|
// DeepSeek
|
|
@@ -113,6 +117,16 @@ var BLOCKRUN_MODELS = [
|
|
|
113
117
|
maxOutput: 128e3,
|
|
114
118
|
reasoning: true
|
|
115
119
|
},
|
|
120
|
+
// OpenAI Codex Family
|
|
121
|
+
{
|
|
122
|
+
id: "openai/gpt-5.2-codex",
|
|
123
|
+
name: "GPT-5.2 Codex",
|
|
124
|
+
inputPrice: 2.5,
|
|
125
|
+
outputPrice: 12,
|
|
126
|
+
contextWindow: 128e3,
|
|
127
|
+
maxOutput: 32e3,
|
|
128
|
+
agentic: true
|
|
129
|
+
},
|
|
116
130
|
// OpenAI GPT-4 Family
|
|
117
131
|
{
|
|
118
132
|
id: "openai/gpt-4.1",
|
|
@@ -218,6 +232,17 @@ var BLOCKRUN_MODELS = [
|
|
|
218
232
|
reasoning: true,
|
|
219
233
|
agentic: true
|
|
220
234
|
},
|
|
235
|
+
{
|
|
236
|
+
id: "anthropic/claude-opus-4.6",
|
|
237
|
+
name: "Claude Opus 4.6",
|
|
238
|
+
inputPrice: 5,
|
|
239
|
+
outputPrice: 25,
|
|
240
|
+
contextWindow: 2e5,
|
|
241
|
+
maxOutput: 64e3,
|
|
242
|
+
reasoning: true,
|
|
243
|
+
vision: true,
|
|
244
|
+
agentic: true
|
|
245
|
+
},
|
|
221
246
|
// Google
|
|
222
247
|
{
|
|
223
248
|
id: "google/gemini-3-pro-preview",
|
|
@@ -1645,27 +1670,43 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1645
1670
|
}
|
|
1646
1671
|
},
|
|
1647
1672
|
// Premium tier configs - best quality (blockrun/premium)
|
|
1648
|
-
// kimi=coding, sonnet=reasoning/instructions, opus=
|
|
1673
|
+
// codex=complex coding, kimi=simple coding, sonnet=reasoning/instructions, opus=architecture/PM/audits
|
|
1649
1674
|
premiumTiers: {
|
|
1650
1675
|
SIMPLE: {
|
|
1651
1676
|
primary: "moonshot/kimi-k2.5",
|
|
1652
|
-
// $0.50/$2.40 - good for coding
|
|
1677
|
+
// $0.50/$2.40 - good for simple coding
|
|
1653
1678
|
fallback: ["anthropic/claude-haiku-4.5", "google/gemini-2.5-flash", "xai/grok-code-fast-1"]
|
|
1654
1679
|
},
|
|
1655
1680
|
MEDIUM: {
|
|
1656
1681
|
primary: "anthropic/claude-sonnet-4",
|
|
1657
1682
|
// $3/$15 - reasoning/instructions
|
|
1658
|
-
fallback: [
|
|
1683
|
+
fallback: [
|
|
1684
|
+
"openai/gpt-5.2-codex",
|
|
1685
|
+
"moonshot/kimi-k2.5",
|
|
1686
|
+
"google/gemini-2.5-pro",
|
|
1687
|
+
"xai/grok-4-0709"
|
|
1688
|
+
]
|
|
1659
1689
|
},
|
|
1660
1690
|
COMPLEX: {
|
|
1661
|
-
primary: "
|
|
1662
|
-
// $
|
|
1663
|
-
fallback: [
|
|
1691
|
+
primary: "openai/gpt-5.2-codex",
|
|
1692
|
+
// $2.50/$10 - complex coding (78% cost savings vs Opus)
|
|
1693
|
+
fallback: [
|
|
1694
|
+
"anthropic/claude-opus-4.6",
|
|
1695
|
+
"anthropic/claude-opus-4.5",
|
|
1696
|
+
"anthropic/claude-sonnet-4",
|
|
1697
|
+
"google/gemini-3-pro-preview",
|
|
1698
|
+
"moonshot/kimi-k2.5"
|
|
1699
|
+
]
|
|
1664
1700
|
},
|
|
1665
1701
|
REASONING: {
|
|
1666
1702
|
primary: "anthropic/claude-sonnet-4",
|
|
1667
1703
|
// $3/$15 - best for reasoning/instructions
|
|
1668
|
-
fallback: [
|
|
1704
|
+
fallback: [
|
|
1705
|
+
"anthropic/claude-opus-4.6",
|
|
1706
|
+
"anthropic/claude-opus-4.5",
|
|
1707
|
+
"openai/o3",
|
|
1708
|
+
"xai/grok-4-1-fast-reasoning"
|
|
1709
|
+
]
|
|
1669
1710
|
}
|
|
1670
1711
|
},
|
|
1671
1712
|
// Agentic tier configs - models that excel at multi-step autonomous tasks
|
|
@@ -1687,7 +1728,7 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1687
1728
|
COMPLEX: {
|
|
1688
1729
|
primary: "anthropic/claude-sonnet-4",
|
|
1689
1730
|
fallback: [
|
|
1690
|
-
"anthropic/claude-opus-4.
|
|
1731
|
+
"anthropic/claude-opus-4.6",
|
|
1691
1732
|
// Latest Opus - best agentic
|
|
1692
1733
|
"openai/gpt-5.2",
|
|
1693
1734
|
"google/gemini-3-pro-preview",
|
|
@@ -1698,7 +1739,7 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1698
1739
|
primary: "anthropic/claude-sonnet-4",
|
|
1699
1740
|
// Strong tool use + reasoning for agentic tasks
|
|
1700
1741
|
fallback: [
|
|
1701
|
-
"anthropic/claude-opus-4.
|
|
1742
|
+
"anthropic/claude-opus-4.6",
|
|
1702
1743
|
"xai/grok-4-fast-reasoning",
|
|
1703
1744
|
"moonshot/kimi-k2.5",
|
|
1704
1745
|
"deepseek/deepseek-reasoner"
|
|
@@ -2128,6 +2169,203 @@ var RequestDeduplicator = class {
|
|
|
2128
2169
|
}
|
|
2129
2170
|
};
|
|
2130
2171
|
|
|
2172
|
+
// src/response-cache.ts
|
|
2173
|
+
import { createHash as createHash2 } from "crypto";
|
|
2174
|
+
var DEFAULT_CONFIG = {
|
|
2175
|
+
maxSize: 200,
|
|
2176
|
+
defaultTTL: 600,
|
|
2177
|
+
maxItemSize: 1048576,
|
|
2178
|
+
// 1MB
|
|
2179
|
+
enabled: true
|
|
2180
|
+
};
|
|
2181
|
+
function canonicalize2(obj) {
|
|
2182
|
+
if (obj === null || typeof obj !== "object") {
|
|
2183
|
+
return obj;
|
|
2184
|
+
}
|
|
2185
|
+
if (Array.isArray(obj)) {
|
|
2186
|
+
return obj.map(canonicalize2);
|
|
2187
|
+
}
|
|
2188
|
+
const sorted = {};
|
|
2189
|
+
for (const key of Object.keys(obj).sort()) {
|
|
2190
|
+
sorted[key] = canonicalize2(obj[key]);
|
|
2191
|
+
}
|
|
2192
|
+
return sorted;
|
|
2193
|
+
}
|
|
2194
|
+
var TIMESTAMP_PATTERN2 = /^\[\w{3}\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+\w+\]\s*/;
|
|
2195
|
+
function normalizeForCache(obj) {
|
|
2196
|
+
const result = {};
|
|
2197
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2198
|
+
if (["stream", "user", "request_id", "x-request-id"].includes(key)) {
|
|
2199
|
+
continue;
|
|
2200
|
+
}
|
|
2201
|
+
if (key === "messages" && Array.isArray(value)) {
|
|
2202
|
+
result[key] = value.map((msg) => {
|
|
2203
|
+
if (typeof msg === "object" && msg !== null) {
|
|
2204
|
+
const m = msg;
|
|
2205
|
+
if (typeof m.content === "string") {
|
|
2206
|
+
return { ...m, content: m.content.replace(TIMESTAMP_PATTERN2, "") };
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
return msg;
|
|
2210
|
+
});
|
|
2211
|
+
} else {
|
|
2212
|
+
result[key] = value;
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
return result;
|
|
2216
|
+
}
|
|
2217
|
+
var ResponseCache = class {
|
|
2218
|
+
cache = /* @__PURE__ */ new Map();
|
|
2219
|
+
expirationHeap = [];
|
|
2220
|
+
config;
|
|
2221
|
+
// Stats for monitoring
|
|
2222
|
+
stats = {
|
|
2223
|
+
hits: 0,
|
|
2224
|
+
misses: 0,
|
|
2225
|
+
evictions: 0
|
|
2226
|
+
};
|
|
2227
|
+
constructor(config = {}) {
|
|
2228
|
+
const filtered = Object.fromEntries(
|
|
2229
|
+
Object.entries(config).filter(([, v]) => v !== void 0)
|
|
2230
|
+
);
|
|
2231
|
+
this.config = { ...DEFAULT_CONFIG, ...filtered };
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Generate cache key from request body.
|
|
2235
|
+
* Hashes: model + messages + temperature + max_tokens + other params
|
|
2236
|
+
*/
|
|
2237
|
+
static generateKey(body) {
|
|
2238
|
+
try {
|
|
2239
|
+
const parsed = JSON.parse(typeof body === "string" ? body : body.toString());
|
|
2240
|
+
const normalized = normalizeForCache(parsed);
|
|
2241
|
+
const canonical = canonicalize2(normalized);
|
|
2242
|
+
const keyContent = JSON.stringify(canonical);
|
|
2243
|
+
return createHash2("sha256").update(keyContent).digest("hex").slice(0, 32);
|
|
2244
|
+
} catch {
|
|
2245
|
+
const content = typeof body === "string" ? body : body.toString();
|
|
2246
|
+
return createHash2("sha256").update(content).digest("hex").slice(0, 32);
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Check if caching is enabled for this request.
|
|
2251
|
+
* Respects cache control headers and request params.
|
|
2252
|
+
*/
|
|
2253
|
+
shouldCache(body, headers) {
|
|
2254
|
+
if (!this.config.enabled) return false;
|
|
2255
|
+
if (headers?.["cache-control"]?.includes("no-cache")) {
|
|
2256
|
+
return false;
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
const parsed = JSON.parse(typeof body === "string" ? body : body.toString());
|
|
2260
|
+
if (parsed.cache === false || parsed.no_cache === true) {
|
|
2261
|
+
return false;
|
|
2262
|
+
}
|
|
2263
|
+
} catch {
|
|
2264
|
+
}
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
/**
|
|
2268
|
+
* Get cached response if available and not expired.
|
|
2269
|
+
*/
|
|
2270
|
+
get(key) {
|
|
2271
|
+
const entry = this.cache.get(key);
|
|
2272
|
+
if (!entry) {
|
|
2273
|
+
this.stats.misses++;
|
|
2274
|
+
return void 0;
|
|
2275
|
+
}
|
|
2276
|
+
if (Date.now() > entry.expiresAt) {
|
|
2277
|
+
this.cache.delete(key);
|
|
2278
|
+
this.stats.misses++;
|
|
2279
|
+
return void 0;
|
|
2280
|
+
}
|
|
2281
|
+
this.stats.hits++;
|
|
2282
|
+
return entry;
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Cache a response with optional custom TTL.
|
|
2286
|
+
*/
|
|
2287
|
+
set(key, response, ttlSeconds) {
|
|
2288
|
+
if (!this.config.enabled || this.config.maxSize <= 0) return;
|
|
2289
|
+
if (response.body.length > this.config.maxItemSize) {
|
|
2290
|
+
console.log(`[ResponseCache] Skipping cache - item too large: ${response.body.length} bytes`);
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
if (response.status >= 400) {
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
if (this.cache.size >= this.config.maxSize) {
|
|
2297
|
+
this.evict();
|
|
2298
|
+
}
|
|
2299
|
+
const now = Date.now();
|
|
2300
|
+
const ttl = ttlSeconds ?? this.config.defaultTTL;
|
|
2301
|
+
const expiresAt = now + ttl * 1e3;
|
|
2302
|
+
const entry = {
|
|
2303
|
+
...response,
|
|
2304
|
+
cachedAt: now,
|
|
2305
|
+
expiresAt
|
|
2306
|
+
};
|
|
2307
|
+
this.cache.set(key, entry);
|
|
2308
|
+
this.expirationHeap.push({ expiresAt, key });
|
|
2309
|
+
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Evict expired and oldest entries to make room.
|
|
2312
|
+
*/
|
|
2313
|
+
evict() {
|
|
2314
|
+
const now = Date.now();
|
|
2315
|
+
this.expirationHeap.sort((a, b) => a.expiresAt - b.expiresAt);
|
|
2316
|
+
while (this.expirationHeap.length > 0) {
|
|
2317
|
+
const oldest = this.expirationHeap[0];
|
|
2318
|
+
const entry = this.cache.get(oldest.key);
|
|
2319
|
+
if (!entry || entry.expiresAt !== oldest.expiresAt) {
|
|
2320
|
+
this.expirationHeap.shift();
|
|
2321
|
+
continue;
|
|
2322
|
+
}
|
|
2323
|
+
if (oldest.expiresAt <= now) {
|
|
2324
|
+
this.cache.delete(oldest.key);
|
|
2325
|
+
this.expirationHeap.shift();
|
|
2326
|
+
this.stats.evictions++;
|
|
2327
|
+
} else {
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
while (this.cache.size >= this.config.maxSize && this.expirationHeap.length > 0) {
|
|
2332
|
+
const oldest = this.expirationHeap.shift();
|
|
2333
|
+
if (this.cache.has(oldest.key)) {
|
|
2334
|
+
this.cache.delete(oldest.key);
|
|
2335
|
+
this.stats.evictions++;
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
/**
|
|
2340
|
+
* Get cache statistics.
|
|
2341
|
+
*/
|
|
2342
|
+
getStats() {
|
|
2343
|
+
const total = this.stats.hits + this.stats.misses;
|
|
2344
|
+
const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) + "%" : "0%";
|
|
2345
|
+
return {
|
|
2346
|
+
size: this.cache.size,
|
|
2347
|
+
maxSize: this.config.maxSize,
|
|
2348
|
+
hits: this.stats.hits,
|
|
2349
|
+
misses: this.stats.misses,
|
|
2350
|
+
evictions: this.stats.evictions,
|
|
2351
|
+
hitRate
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
/**
|
|
2355
|
+
* Clear all cached entries.
|
|
2356
|
+
*/
|
|
2357
|
+
clear() {
|
|
2358
|
+
this.cache.clear();
|
|
2359
|
+
this.expirationHeap = [];
|
|
2360
|
+
}
|
|
2361
|
+
/**
|
|
2362
|
+
* Check if cache is enabled.
|
|
2363
|
+
*/
|
|
2364
|
+
isEnabled() {
|
|
2365
|
+
return this.config.enabled;
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2131
2369
|
// src/balance.ts
|
|
2132
2370
|
import { createPublicClient, http, erc20Abi } from "viem";
|
|
2133
2371
|
import { base } from "viem/chains";
|
|
@@ -3631,6 +3869,7 @@ async function startProxy(options) {
|
|
|
3631
3869
|
modelPricing
|
|
3632
3870
|
};
|
|
3633
3871
|
const deduplicator = new RequestDeduplicator();
|
|
3872
|
+
const responseCache = new ResponseCache(options.cacheConfig);
|
|
3634
3873
|
const sessionStore = new SessionStore(options.sessionConfig);
|
|
3635
3874
|
const connections = /* @__PURE__ */ new Set();
|
|
3636
3875
|
const server = createServer(async (req, res) => {
|
|
@@ -3671,6 +3910,15 @@ async function startProxy(options) {
|
|
|
3671
3910
|
res.end(JSON.stringify(response));
|
|
3672
3911
|
return;
|
|
3673
3912
|
}
|
|
3913
|
+
if (req.url === "/cache" || req.url?.startsWith("/cache?")) {
|
|
3914
|
+
const stats = responseCache.getStats();
|
|
3915
|
+
res.writeHead(200, {
|
|
3916
|
+
"Content-Type": "application/json",
|
|
3917
|
+
"Cache-Control": "no-cache"
|
|
3918
|
+
});
|
|
3919
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
3920
|
+
return;
|
|
3921
|
+
}
|
|
3674
3922
|
if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
|
|
3675
3923
|
try {
|
|
3676
3924
|
const url = new URL(req.url, "http://localhost");
|
|
@@ -3717,7 +3965,8 @@ async function startProxy(options) {
|
|
|
3717
3965
|
routerOpts,
|
|
3718
3966
|
deduplicator,
|
|
3719
3967
|
balanceMonitor,
|
|
3720
|
-
sessionStore
|
|
3968
|
+
sessionStore,
|
|
3969
|
+
responseCache
|
|
3721
3970
|
);
|
|
3722
3971
|
} catch (err) {
|
|
3723
3972
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -3918,7 +4167,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
3918
4167
|
};
|
|
3919
4168
|
}
|
|
3920
4169
|
}
|
|
3921
|
-
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore) {
|
|
4170
|
+
async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, deduplicator, balanceMonitor, sessionStore, responseCache) {
|
|
3922
4171
|
const startTime = Date.now();
|
|
3923
4172
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
3924
4173
|
const bodyChunks = [];
|
|
@@ -4086,6 +4335,20 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4086
4335
|
);
|
|
4087
4336
|
}
|
|
4088
4337
|
}
|
|
4338
|
+
const cacheKey = ResponseCache.generateKey(body);
|
|
4339
|
+
const reqHeaders = {};
|
|
4340
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
4341
|
+
if (typeof value === "string") reqHeaders[key] = value;
|
|
4342
|
+
}
|
|
4343
|
+
if (responseCache.shouldCache(body, reqHeaders)) {
|
|
4344
|
+
const cachedResponse = responseCache.get(cacheKey);
|
|
4345
|
+
if (cachedResponse) {
|
|
4346
|
+
console.log(`[ClawRouter] Cache HIT for ${cachedResponse.model} (saved API call)`);
|
|
4347
|
+
res.writeHead(cachedResponse.status, cachedResponse.headers);
|
|
4348
|
+
res.end(cachedResponse.body);
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4089
4352
|
const dedupKey = RequestDeduplicator.hash(body);
|
|
4090
4353
|
const cached = deduplicator.getCached(dedupKey);
|
|
4091
4354
|
if (cached) {
|
|
@@ -4438,12 +4701,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4438
4701
|
}
|
|
4439
4702
|
}
|
|
4440
4703
|
res.end();
|
|
4704
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
4441
4705
|
deduplicator.complete(dedupKey, {
|
|
4442
4706
|
status: upstream.status,
|
|
4443
4707
|
headers: responseHeaders,
|
|
4444
|
-
body:
|
|
4708
|
+
body: responseBody,
|
|
4445
4709
|
completedAt: Date.now()
|
|
4446
4710
|
});
|
|
4711
|
+
if (upstream.status === 200 && responseCache.shouldCache(body)) {
|
|
4712
|
+
responseCache.set(cacheKey, {
|
|
4713
|
+
body: responseBody,
|
|
4714
|
+
status: upstream.status,
|
|
4715
|
+
headers: responseHeaders,
|
|
4716
|
+
model: modelId
|
|
4717
|
+
});
|
|
4718
|
+
console.log(`[ClawRouter] Cached response for ${modelId} (${responseBody.length} bytes)`);
|
|
4719
|
+
}
|
|
4447
4720
|
}
|
|
4448
4721
|
if (estimatedCostMicros !== void 0) {
|
|
4449
4722
|
balanceMonitor.deductEstimated(estimatedCostMicros);
|
|
@@ -4750,9 +5023,9 @@ function injectModelsConfig(logger) {
|
|
|
4750
5023
|
{ id: "eco", alias: "eco" },
|
|
4751
5024
|
{ id: "premium", alias: "premium" },
|
|
4752
5025
|
{ id: "free", alias: "free" },
|
|
4753
|
-
{ id: "sonnet", alias: "sonnet" },
|
|
4754
|
-
{ id: "opus", alias: "opus" },
|
|
4755
|
-
{ id: "haiku", alias: "haiku" },
|
|
5026
|
+
{ id: "sonnet", alias: "br-sonnet" },
|
|
5027
|
+
{ id: "opus", alias: "br-opus" },
|
|
5028
|
+
{ id: "haiku", alias: "br-haiku" },
|
|
4756
5029
|
{ id: "gpt5", alias: "gpt5" },
|
|
4757
5030
|
{ id: "mini", alias: "mini" },
|
|
4758
5031
|
{ id: "grok-fast", alias: "grok-fast" },
|
|
@@ -4778,9 +5051,13 @@ function injectModelsConfig(logger) {
|
|
|
4778
5051
|
}
|
|
4779
5052
|
for (const m of KEY_MODEL_ALIASES) {
|
|
4780
5053
|
const fullId = `blockrun/${m.id}`;
|
|
4781
|
-
|
|
5054
|
+
const existing = allowlist[fullId];
|
|
5055
|
+
if (!existing) {
|
|
4782
5056
|
allowlist[fullId] = { alias: m.alias };
|
|
4783
5057
|
needsWrite = true;
|
|
5058
|
+
} else if (existing.alias !== m.alias) {
|
|
5059
|
+
existing.alias = m.alias;
|
|
5060
|
+
needsWrite = true;
|
|
4784
5061
|
}
|
|
4785
5062
|
}
|
|
4786
5063
|
if (needsWrite) {
|
|
@@ -5114,6 +5391,7 @@ export {
|
|
|
5114
5391
|
OPENCLAW_MODELS,
|
|
5115
5392
|
PaymentCache,
|
|
5116
5393
|
RequestDeduplicator,
|
|
5394
|
+
ResponseCache,
|
|
5117
5395
|
RpcError,
|
|
5118
5396
|
SessionStore,
|
|
5119
5397
|
blockrunProvider,
|