@blockrun/clawrouter 0.9.7 → 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 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +92 -1
- package/dist/index.js +295 -28
- 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,37 +1670,42 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1645
1670
|
}
|
|
1646
1671
|
},
|
|
1647
1672
|
// Premium tier configs - best quality (blockrun/premium)
|
|
1673
|
+
// codex=complex coding, kimi=simple coding, sonnet=reasoning/instructions, opus=architecture/PM/audits
|
|
1648
1674
|
premiumTiers: {
|
|
1649
1675
|
SIMPLE: {
|
|
1650
|
-
primary: "
|
|
1651
|
-
// $0.
|
|
1652
|
-
fallback: ["
|
|
1676
|
+
primary: "moonshot/kimi-k2.5",
|
|
1677
|
+
// $0.50/$2.40 - good for simple coding
|
|
1678
|
+
fallback: ["anthropic/claude-haiku-4.5", "google/gemini-2.5-flash", "xai/grok-code-fast-1"]
|
|
1653
1679
|
},
|
|
1654
1680
|
MEDIUM: {
|
|
1655
|
-
primary: "
|
|
1656
|
-
// $
|
|
1657
|
-
fallback: [
|
|
1681
|
+
primary: "anthropic/claude-sonnet-4",
|
|
1682
|
+
// $3/$15 - reasoning/instructions
|
|
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
|
+
]
|
|
1658
1689
|
},
|
|
1659
1690
|
COMPLEX: {
|
|
1660
|
-
primary: "
|
|
1661
|
-
// $
|
|
1691
|
+
primary: "openai/gpt-5.2-codex",
|
|
1692
|
+
// $2.50/$10 - complex coding (78% cost savings vs Opus)
|
|
1662
1693
|
fallback: [
|
|
1663
|
-
"
|
|
1664
|
-
|
|
1694
|
+
"anthropic/claude-opus-4.6",
|
|
1695
|
+
"anthropic/claude-opus-4.5",
|
|
1696
|
+
"anthropic/claude-sonnet-4",
|
|
1665
1697
|
"google/gemini-3-pro-preview",
|
|
1666
|
-
|
|
1667
|
-
"openai/gpt-5.2",
|
|
1668
|
-
"anthropic/claude-sonnet-4"
|
|
1698
|
+
"moonshot/kimi-k2.5"
|
|
1669
1699
|
]
|
|
1670
1700
|
},
|
|
1671
1701
|
REASONING: {
|
|
1672
|
-
primary: "
|
|
1673
|
-
// $
|
|
1702
|
+
primary: "anthropic/claude-sonnet-4",
|
|
1703
|
+
// $3/$15 - best for reasoning/instructions
|
|
1674
1704
|
fallback: [
|
|
1675
|
-
"
|
|
1676
|
-
// Latest o-series
|
|
1705
|
+
"anthropic/claude-opus-4.6",
|
|
1677
1706
|
"anthropic/claude-opus-4.5",
|
|
1678
|
-
"
|
|
1707
|
+
"openai/o3",
|
|
1708
|
+
"xai/grok-4-1-fast-reasoning"
|
|
1679
1709
|
]
|
|
1680
1710
|
}
|
|
1681
1711
|
},
|
|
@@ -1698,7 +1728,7 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1698
1728
|
COMPLEX: {
|
|
1699
1729
|
primary: "anthropic/claude-sonnet-4",
|
|
1700
1730
|
fallback: [
|
|
1701
|
-
"anthropic/claude-opus-4.
|
|
1731
|
+
"anthropic/claude-opus-4.6",
|
|
1702
1732
|
// Latest Opus - best agentic
|
|
1703
1733
|
"openai/gpt-5.2",
|
|
1704
1734
|
"google/gemini-3-pro-preview",
|
|
@@ -1709,7 +1739,7 @@ var DEFAULT_ROUTING_CONFIG = {
|
|
|
1709
1739
|
primary: "anthropic/claude-sonnet-4",
|
|
1710
1740
|
// Strong tool use + reasoning for agentic tasks
|
|
1711
1741
|
fallback: [
|
|
1712
|
-
"anthropic/claude-opus-4.
|
|
1742
|
+
"anthropic/claude-opus-4.6",
|
|
1713
1743
|
"xai/grok-4-fast-reasoning",
|
|
1714
1744
|
"moonshot/kimi-k2.5",
|
|
1715
1745
|
"deepseek/deepseek-reasoner"
|
|
@@ -2139,6 +2169,203 @@ var RequestDeduplicator = class {
|
|
|
2139
2169
|
}
|
|
2140
2170
|
};
|
|
2141
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
|
+
|
|
2142
2369
|
// src/balance.ts
|
|
2143
2370
|
import { createPublicClient, http, erc20Abi } from "viem";
|
|
2144
2371
|
import { base } from "viem/chains";
|
|
@@ -3642,6 +3869,7 @@ async function startProxy(options) {
|
|
|
3642
3869
|
modelPricing
|
|
3643
3870
|
};
|
|
3644
3871
|
const deduplicator = new RequestDeduplicator();
|
|
3872
|
+
const responseCache = new ResponseCache(options.cacheConfig);
|
|
3645
3873
|
const sessionStore = new SessionStore(options.sessionConfig);
|
|
3646
3874
|
const connections = /* @__PURE__ */ new Set();
|
|
3647
3875
|
const server = createServer(async (req, res) => {
|
|
@@ -3682,6 +3910,15 @@ async function startProxy(options) {
|
|
|
3682
3910
|
res.end(JSON.stringify(response));
|
|
3683
3911
|
return;
|
|
3684
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
|
+
}
|
|
3685
3922
|
if (req.url === "/stats" || req.url?.startsWith("/stats?")) {
|
|
3686
3923
|
try {
|
|
3687
3924
|
const url = new URL(req.url, "http://localhost");
|
|
@@ -3728,7 +3965,8 @@ async function startProxy(options) {
|
|
|
3728
3965
|
routerOpts,
|
|
3729
3966
|
deduplicator,
|
|
3730
3967
|
balanceMonitor,
|
|
3731
|
-
sessionStore
|
|
3968
|
+
sessionStore,
|
|
3969
|
+
responseCache
|
|
3732
3970
|
);
|
|
3733
3971
|
} catch (err) {
|
|
3734
3972
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -3929,7 +4167,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
3929
4167
|
};
|
|
3930
4168
|
}
|
|
3931
4169
|
}
|
|
3932
|
-
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) {
|
|
3933
4171
|
const startTime = Date.now();
|
|
3934
4172
|
const upstreamUrl = `${apiBase}${req.url}`;
|
|
3935
4173
|
const bodyChunks = [];
|
|
@@ -4097,6 +4335,20 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4097
4335
|
);
|
|
4098
4336
|
}
|
|
4099
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
|
+
}
|
|
4100
4352
|
const dedupKey = RequestDeduplicator.hash(body);
|
|
4101
4353
|
const cached = deduplicator.getCached(dedupKey);
|
|
4102
4354
|
if (cached) {
|
|
@@ -4449,12 +4701,22 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
4449
4701
|
}
|
|
4450
4702
|
}
|
|
4451
4703
|
res.end();
|
|
4704
|
+
const responseBody = Buffer.concat(responseChunks);
|
|
4452
4705
|
deduplicator.complete(dedupKey, {
|
|
4453
4706
|
status: upstream.status,
|
|
4454
4707
|
headers: responseHeaders,
|
|
4455
|
-
body:
|
|
4708
|
+
body: responseBody,
|
|
4456
4709
|
completedAt: Date.now()
|
|
4457
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
|
+
}
|
|
4458
4720
|
}
|
|
4459
4721
|
if (estimatedCostMicros !== void 0) {
|
|
4460
4722
|
balanceMonitor.deductEstimated(estimatedCostMicros);
|
|
@@ -4761,9 +5023,9 @@ function injectModelsConfig(logger) {
|
|
|
4761
5023
|
{ id: "eco", alias: "eco" },
|
|
4762
5024
|
{ id: "premium", alias: "premium" },
|
|
4763
5025
|
{ id: "free", alias: "free" },
|
|
4764
|
-
{ id: "sonnet", alias: "sonnet" },
|
|
4765
|
-
{ id: "opus", alias: "opus" },
|
|
4766
|
-
{ id: "haiku", alias: "haiku" },
|
|
5026
|
+
{ id: "sonnet", alias: "br-sonnet" },
|
|
5027
|
+
{ id: "opus", alias: "br-opus" },
|
|
5028
|
+
{ id: "haiku", alias: "br-haiku" },
|
|
4767
5029
|
{ id: "gpt5", alias: "gpt5" },
|
|
4768
5030
|
{ id: "mini", alias: "mini" },
|
|
4769
5031
|
{ id: "grok-fast", alias: "grok-fast" },
|
|
@@ -4789,9 +5051,13 @@ function injectModelsConfig(logger) {
|
|
|
4789
5051
|
}
|
|
4790
5052
|
for (const m of KEY_MODEL_ALIASES) {
|
|
4791
5053
|
const fullId = `blockrun/${m.id}`;
|
|
4792
|
-
|
|
5054
|
+
const existing = allowlist[fullId];
|
|
5055
|
+
if (!existing) {
|
|
4793
5056
|
allowlist[fullId] = { alias: m.alias };
|
|
4794
5057
|
needsWrite = true;
|
|
5058
|
+
} else if (existing.alias !== m.alias) {
|
|
5059
|
+
existing.alias = m.alias;
|
|
5060
|
+
needsWrite = true;
|
|
4795
5061
|
}
|
|
4796
5062
|
}
|
|
4797
5063
|
if (needsWrite) {
|
|
@@ -5125,6 +5391,7 @@ export {
|
|
|
5125
5391
|
OPENCLAW_MODELS,
|
|
5126
5392
|
PaymentCache,
|
|
5127
5393
|
RequestDeduplicator,
|
|
5394
|
+
ResponseCache,
|
|
5128
5395
|
RpcError,
|
|
5129
5396
|
SessionStore,
|
|
5130
5397
|
blockrunProvider,
|