@blockrun/clawrouter 0.12.38 → 0.12.40

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
@@ -128,6 +128,42 @@ type OpenClawPluginDefinition = {
128
128
  activate?: (api: OpenClawPluginApi) => void | Promise<void>;
129
129
  };
130
130
 
131
+ /**
132
+ * Tier → Model Selection
133
+ *
134
+ * Maps a classification tier to the cheapest capable model.
135
+ * Builds RoutingDecision metadata with cost estimates and savings.
136
+ */
137
+
138
+ type ModelPricing = {
139
+ inputPrice: number;
140
+ outputPrice: number;
141
+ };
142
+ /**
143
+ * Get the ordered fallback chain for a tier: [primary, ...fallbacks].
144
+ */
145
+ declare function getFallbackChain(tier: Tier, tierConfigs: Record<Tier, TierConfig>): string[];
146
+ /**
147
+ * Calculate cost for a specific model (used when fallback model is used).
148
+ * Returns updated cost fields for RoutingDecision.
149
+ */
150
+ declare function calculateModelCost(model: string, modelPricing: Map<string, ModelPricing>, estimatedInputTokens: number, maxOutputTokens: number, routingProfile?: "free" | "eco" | "auto" | "premium"): {
151
+ costEstimate: number;
152
+ baselineCost: number;
153
+ savings: number;
154
+ };
155
+ /**
156
+ * Get the fallback chain filtered by context length.
157
+ * Only returns models that can handle the estimated total context.
158
+ *
159
+ * @param tier - The tier to get fallback chain for
160
+ * @param tierConfigs - Tier configurations
161
+ * @param estimatedTotalTokens - Estimated total context (input + output)
162
+ * @param getContextWindow - Function to get context window for a model ID
163
+ * @returns Filtered list of models that can handle the context
164
+ */
165
+ declare function getFallbackChainFiltered(tier: Tier, tierConfigs: Record<Tier, TierConfig>, estimatedTotalTokens: number, getContextWindow: (modelId: string) => number | undefined): string[];
166
+
131
167
  /**
132
168
  * Smart Router Types
133
169
  *
@@ -148,6 +184,16 @@ type RoutingDecision = {
148
184
  baselineCost: number;
149
185
  savings: number;
150
186
  agenticScore?: number;
187
+ /** Which tier configs were used (auto/eco/premium/agentic) — avoids re-derivation in proxy */
188
+ tierConfigs?: Record<Tier, TierConfig>;
189
+ /** Which routing profile was applied */
190
+ profile?: "auto" | "eco" | "premium" | "agentic";
191
+ };
192
+ type RouterOptions = {
193
+ config: RoutingConfig;
194
+ modelPricing: Map<string, ModelPricing>;
195
+ routingProfile?: "free" | "eco" | "auto" | "premium";
196
+ hasTools?: boolean;
151
197
  };
152
198
  type TierConfig = {
153
199
  primary: string;
@@ -211,42 +257,6 @@ type RoutingConfig = {
211
257
  overrides: OverridesConfig;
212
258
  };
213
259
 
214
- /**
215
- * Tier → Model Selection
216
- *
217
- * Maps a classification tier to the cheapest capable model.
218
- * Builds RoutingDecision metadata with cost estimates and savings.
219
- */
220
-
221
- type ModelPricing = {
222
- inputPrice: number;
223
- outputPrice: number;
224
- };
225
- /**
226
- * Get the ordered fallback chain for a tier: [primary, ...fallbacks].
227
- */
228
- declare function getFallbackChain(tier: Tier, tierConfigs: Record<Tier, TierConfig>): string[];
229
- /**
230
- * Calculate cost for a specific model (used when fallback model is used).
231
- * Returns updated cost fields for RoutingDecision.
232
- */
233
- declare function calculateModelCost(model: string, modelPricing: Map<string, ModelPricing>, estimatedInputTokens: number, maxOutputTokens: number, routingProfile?: "free" | "eco" | "auto" | "premium"): {
234
- costEstimate: number;
235
- baselineCost: number;
236
- savings: number;
237
- };
238
- /**
239
- * Get the fallback chain filtered by context length.
240
- * Only returns models that can handle the estimated total context.
241
- *
242
- * @param tier - The tier to get fallback chain for
243
- * @param tierConfigs - Tier configurations
244
- * @param estimatedTotalTokens - Estimated total context (input + output)
245
- * @param getContextWindow - Function to get context window for a model ID
246
- * @returns Filtered list of models that can handle the context
247
- */
248
- declare function getFallbackChainFiltered(tier: Tier, tierConfigs: Record<Tier, TierConfig>, estimatedTotalTokens: number, getContextWindow: (modelId: string) => number | undefined): string[];
249
-
250
260
  /**
251
261
  * Default Routing Config
252
262
  *
@@ -262,24 +272,12 @@ declare const DEFAULT_ROUTING_CONFIG: RoutingConfig;
262
272
  * Smart Router Entry Point
263
273
  *
264
274
  * Classifies requests and routes to the cheapest capable model.
265
- * 100% local rules-based scoring handles all requests in <1ms.
266
- * Ambiguous cases default to configurable tier (MEDIUM by default).
275
+ * Delegates to pluggable RouterStrategy (default: RulesStrategy, <1ms).
267
276
  */
268
277
 
269
- type RouterOptions = {
270
- config: RoutingConfig;
271
- modelPricing: Map<string, ModelPricing>;
272
- routingProfile?: "free" | "eco" | "auto" | "premium";
273
- hasTools?: boolean;
274
- };
275
278
  /**
276
279
  * Route a request to the cheapest capable model.
277
- *
278
- * 1. Check overrides (large context, structured output)
279
- * 2. Run rule-based classifier (14 weighted dimensions, <1ms)
280
- * 3. If ambiguous, default to configurable tier (no external API calls)
281
- * 4. Select model for tier
282
- * 5. Return RoutingDecision with metadata
280
+ * Delegates to the registered "rules" strategy by default.
283
281
  */
284
282
  declare function route(prompt: string, systemPrompt: string | undefined, maxOutputTokens: number, options: RouterOptions): RoutingDecision;
285
283
 
package/dist/index.js CHANGED
@@ -2022,6 +2022,99 @@ function getFallbackChainFiltered(tier, tierConfigs, estimatedTotalTokens, getCo
2022
2022
  return filtered;
2023
2023
  }
2024
2024
 
2025
+ // src/router/strategy.ts
2026
+ var RulesStrategy = class {
2027
+ name = "rules";
2028
+ route(prompt, systemPrompt, maxOutputTokens, options) {
2029
+ const { config, modelPricing } = options;
2030
+ const fullText = `${systemPrompt ?? ""} ${prompt}`;
2031
+ const estimatedTokens = Math.ceil(fullText.length / 4);
2032
+ const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
2033
+ const { routingProfile } = options;
2034
+ let tierConfigs;
2035
+ let profileSuffix;
2036
+ let profile;
2037
+ if (routingProfile === "eco" && config.ecoTiers) {
2038
+ tierConfigs = config.ecoTiers;
2039
+ profileSuffix = " | eco";
2040
+ profile = "eco";
2041
+ } else if (routingProfile === "premium" && config.premiumTiers) {
2042
+ tierConfigs = config.premiumTiers;
2043
+ profileSuffix = " | premium";
2044
+ profile = "premium";
2045
+ } else {
2046
+ const agenticScore = ruleResult.agenticScore ?? 0;
2047
+ const isAutoAgentic = agenticScore >= 0.5;
2048
+ const isExplicitAgentic = config.overrides.agenticMode ?? false;
2049
+ const hasToolsInRequest = options.hasTools ?? false;
2050
+ const useAgenticTiers = (hasToolsInRequest || isAutoAgentic || isExplicitAgentic) && config.agenticTiers != null;
2051
+ tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
2052
+ profileSuffix = useAgenticTiers ? ` | agentic${hasToolsInRequest ? " (tools)" : ""}` : "";
2053
+ profile = useAgenticTiers ? "agentic" : "auto";
2054
+ }
2055
+ const agenticScoreValue = ruleResult.agenticScore;
2056
+ if (estimatedTokens > config.overrides.maxTokensForceComplex) {
2057
+ const decision2 = selectModel(
2058
+ "COMPLEX",
2059
+ 0.95,
2060
+ "rules",
2061
+ `Input exceeds ${config.overrides.maxTokensForceComplex} tokens${profileSuffix}`,
2062
+ tierConfigs,
2063
+ modelPricing,
2064
+ estimatedTokens,
2065
+ maxOutputTokens,
2066
+ routingProfile,
2067
+ agenticScoreValue
2068
+ );
2069
+ return { ...decision2, tierConfigs, profile };
2070
+ }
2071
+ const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
2072
+ let tier;
2073
+ let confidence;
2074
+ const method = "rules";
2075
+ let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
2076
+ if (ruleResult.tier !== null) {
2077
+ tier = ruleResult.tier;
2078
+ confidence = ruleResult.confidence;
2079
+ } else {
2080
+ tier = config.overrides.ambiguousDefaultTier;
2081
+ confidence = 0.5;
2082
+ reasoning += ` | ambiguous -> default: ${tier}`;
2083
+ }
2084
+ if (hasStructuredOutput) {
2085
+ const tierRank = { SIMPLE: 0, MEDIUM: 1, COMPLEX: 2, REASONING: 3 };
2086
+ const minTier = config.overrides.structuredOutputMinTier;
2087
+ if (tierRank[tier] < tierRank[minTier]) {
2088
+ reasoning += ` | upgraded to ${minTier} (structured output)`;
2089
+ tier = minTier;
2090
+ }
2091
+ }
2092
+ reasoning += profileSuffix;
2093
+ const decision = selectModel(
2094
+ tier,
2095
+ confidence,
2096
+ method,
2097
+ reasoning,
2098
+ tierConfigs,
2099
+ modelPricing,
2100
+ estimatedTokens,
2101
+ maxOutputTokens,
2102
+ routingProfile,
2103
+ agenticScoreValue
2104
+ );
2105
+ return { ...decision, tierConfigs, profile };
2106
+ }
2107
+ };
2108
+ var registry = /* @__PURE__ */ new Map();
2109
+ registry.set("rules", new RulesStrategy());
2110
+ function getStrategy(name) {
2111
+ const strategy = registry.get(name);
2112
+ if (!strategy) {
2113
+ throw new Error(`Unknown routing strategy: ${name}`);
2114
+ }
2115
+ return strategy;
2116
+ }
2117
+
2025
2118
  // src/router/config.ts
2026
2119
  var DEFAULT_ROUTING_CONFIG = {
2027
2120
  version: "2.0",
@@ -3114,7 +3207,11 @@ var DEFAULT_ROUTING_CONFIG = {
3114
3207
  SIMPLE: {
3115
3208
  primary: "nvidia/gpt-oss-120b",
3116
3209
  // FREE! $0.00/$0.00
3117
- fallback: ["google/gemini-2.5-flash-lite", "google/gemini-2.5-flash", "deepseek/deepseek-chat"]
3210
+ fallback: [
3211
+ "google/gemini-2.5-flash-lite",
3212
+ "google/gemini-2.5-flash",
3213
+ "deepseek/deepseek-chat"
3214
+ ]
3118
3215
  },
3119
3216
  MEDIUM: {
3120
3217
  primary: "google/gemini-2.5-flash-lite",
@@ -3239,77 +3336,8 @@ var DEFAULT_ROUTING_CONFIG = {
3239
3336
 
3240
3337
  // src/router/index.ts
3241
3338
  function route(prompt, systemPrompt, maxOutputTokens, options) {
3242
- const { config, modelPricing } = options;
3243
- const fullText = `${systemPrompt ?? ""} ${prompt}`;
3244
- const estimatedTokens = Math.ceil(fullText.length / 4);
3245
- const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
3246
- const { routingProfile } = options;
3247
- let tierConfigs;
3248
- let profileSuffix;
3249
- if (routingProfile === "eco" && config.ecoTiers) {
3250
- tierConfigs = config.ecoTiers;
3251
- profileSuffix = " | eco";
3252
- } else if (routingProfile === "premium" && config.premiumTiers) {
3253
- tierConfigs = config.premiumTiers;
3254
- profileSuffix = " | premium";
3255
- } else {
3256
- const agenticScore = ruleResult.agenticScore ?? 0;
3257
- const isAutoAgentic = agenticScore >= 0.5;
3258
- const isExplicitAgentic = config.overrides.agenticMode ?? false;
3259
- const hasToolsInRequest = options.hasTools ?? false;
3260
- const useAgenticTiers = (hasToolsInRequest || isAutoAgentic || isExplicitAgentic) && config.agenticTiers != null;
3261
- tierConfigs = useAgenticTiers ? config.agenticTiers : config.tiers;
3262
- profileSuffix = useAgenticTiers ? ` | agentic${hasToolsInRequest ? " (tools)" : ""}` : "";
3263
- }
3264
- const agenticScoreValue = ruleResult.agenticScore;
3265
- if (estimatedTokens > config.overrides.maxTokensForceComplex) {
3266
- return selectModel(
3267
- "COMPLEX",
3268
- 0.95,
3269
- "rules",
3270
- `Input exceeds ${config.overrides.maxTokensForceComplex} tokens${profileSuffix}`,
3271
- tierConfigs,
3272
- modelPricing,
3273
- estimatedTokens,
3274
- maxOutputTokens,
3275
- routingProfile,
3276
- agenticScoreValue
3277
- );
3278
- }
3279
- const hasStructuredOutput = systemPrompt ? /json|structured|schema/i.test(systemPrompt) : false;
3280
- let tier;
3281
- let confidence;
3282
- const method = "rules";
3283
- let reasoning = `score=${ruleResult.score.toFixed(2)} | ${ruleResult.signals.join(", ")}`;
3284
- if (ruleResult.tier !== null) {
3285
- tier = ruleResult.tier;
3286
- confidence = ruleResult.confidence;
3287
- } else {
3288
- tier = config.overrides.ambiguousDefaultTier;
3289
- confidence = 0.5;
3290
- reasoning += ` | ambiguous -> default: ${tier}`;
3291
- }
3292
- if (hasStructuredOutput) {
3293
- const tierRank = { SIMPLE: 0, MEDIUM: 1, COMPLEX: 2, REASONING: 3 };
3294
- const minTier = config.overrides.structuredOutputMinTier;
3295
- if (tierRank[tier] < tierRank[minTier]) {
3296
- reasoning += ` | upgraded to ${minTier} (structured output)`;
3297
- tier = minTier;
3298
- }
3299
- }
3300
- reasoning += profileSuffix;
3301
- return selectModel(
3302
- tier,
3303
- confidence,
3304
- method,
3305
- reasoning,
3306
- tierConfigs,
3307
- modelPricing,
3308
- estimatedTokens,
3309
- maxOutputTokens,
3310
- routingProfile,
3311
- agenticScoreValue
3312
- );
3339
+ const strategy = getStrategy("rules");
3340
+ return strategy.route(prompt, systemPrompt, maxOutputTokens, options);
3313
3341
  }
3314
3342
 
3315
3343
  // src/logger.ts
@@ -5454,6 +5482,12 @@ var ROUTING_PROFILES = /* @__PURE__ */ new Set([
5454
5482
  "premium"
5455
5483
  ]);
5456
5484
  var FREE_MODEL = "nvidia/gpt-oss-120b";
5485
+ var FREE_TIER_CONFIGS = {
5486
+ SIMPLE: { primary: FREE_MODEL, fallback: [] },
5487
+ MEDIUM: { primary: FREE_MODEL, fallback: [] },
5488
+ COMPLEX: { primary: FREE_MODEL, fallback: [] },
5489
+ REASONING: { primary: FREE_MODEL, fallback: [] }
5490
+ };
5457
5491
  var freeRequestCount = 0;
5458
5492
  var MAX_MESSAGES = 200;
5459
5493
  var CONTEXT_LIMIT_KB = 5120;
@@ -7328,16 +7362,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7328
7362
 
7329
7363
  `;
7330
7364
  }
7331
- await logUsage({
7332
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7365
+ routingDecision = {
7333
7366
  model: freeModel,
7334
7367
  tier: "SIMPLE",
7335
- cost: 0,
7368
+ confidence: 1,
7369
+ method: "rules",
7370
+ reasoning: "free profile",
7371
+ costEstimate: 0,
7336
7372
  baselineCost: 0,
7337
7373
  savings: 1,
7338
- // 100% savings
7339
- latencyMs: 0
7340
- });
7374
+ tierConfigs: FREE_TIER_CONFIGS
7375
+ };
7341
7376
  } else {
7342
7377
  effectiveSessionId = getSessionId(req.headers) ?? deriveSessionId(parsedMessages);
7343
7378
  const existingSession = effectiveSessionId ? sessionStore.getSession(effectiveSessionId) : void 0;
@@ -7428,18 +7463,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7428
7463
  const contentHash = hashRequestContent(prompt, toolCallNames);
7429
7464
  const shouldEscalate = sessionStore.recordRequestHash(effectiveSessionId, contentHash);
7430
7465
  if (shouldEscalate) {
7431
- const activeTierConfigs = (() => {
7432
- if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
7433
- return routerOpts.config.agenticTiers;
7434
- }
7435
- if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
7436
- return routerOpts.config.ecoTiers;
7437
- }
7438
- if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
7439
- return routerOpts.config.premiumTiers;
7440
- }
7441
- return routerOpts.config.tiers;
7442
- })();
7466
+ const activeTierConfigs = routingDecision.tierConfigs ?? routerOpts.config.tiers;
7443
7467
  const escalation = sessionStore.escalateSession(
7444
7468
  effectiveSessionId,
7445
7469
  activeTierConfigs
@@ -7655,18 +7679,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7655
7679
  if (routingDecision) {
7656
7680
  const estimatedInputTokens = Math.ceil(body.length / 4);
7657
7681
  const estimatedTotalTokens = estimatedInputTokens + maxTokens;
7658
- const tierConfigs = (() => {
7659
- if (routingDecision.reasoning?.includes("agentic") && routerOpts.config.agenticTiers) {
7660
- return routerOpts.config.agenticTiers;
7661
- }
7662
- if (routingProfile === "eco" && routerOpts.config.ecoTiers) {
7663
- return routerOpts.config.ecoTiers;
7664
- }
7665
- if (routingProfile === "premium" && routerOpts.config.premiumTiers) {
7666
- return routerOpts.config.premiumTiers;
7667
- }
7668
- return routerOpts.config.tiers;
7669
- })();
7682
+ const tierConfigs = routingDecision.tierConfigs ?? routerOpts.config.tiers;
7670
7683
  const fullChain = getFallbackChain(routingDecision.tier, tierConfigs);
7671
7684
  const contextFiltered = getFallbackChainFiltered(
7672
7685
  routingDecision.tier,
@@ -7746,6 +7759,14 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
7746
7759
  status: result.errorStatus || 500
7747
7760
  };
7748
7761
  if (result.isProviderError && !isLastAttempt) {
7762
+ const isExplicitModelError = !routingDecision;
7763
+ const isUnknownExplicitModel = isExplicitModelError && /unknown.*model|invalid.*model/i.test(result.errorBody || "");
7764
+ if (isUnknownExplicitModel) {
7765
+ console.log(
7766
+ `[ClawRouter] Explicit model error from ${tryModel}, not falling back: ${result.errorBody?.slice(0, 100)}`
7767
+ );
7768
+ break;
7769
+ }
7749
7770
  if (result.errorStatus === 429) {
7750
7771
  markRateLimited(tryModel);
7751
7772
  try {