Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,37 @@ All notable changes to ClawRouter.

---

## v0.12.242 — August 7, 2026

Makes the deterministic **Router v3.4 portfolio strategy the default for Auto**, and moves the routing engine out of this repo into [`BlockRunAI/router-core`](https://github.com/BlockRunAI/router-core).

Thanks to @KillerQueen-Z for the router work and the extraction (#238).

### Changed — portfolio routing is the Auto default

- Auto now classifies task shape locally, enforces tool / vision / structured-output / context constraints as hard filters, then ranks an ordered fallback portfolio. The previous path picked a fixed tier primary after rule classification. On a frozen 100-task, three-arm evaluation the new policy scored 57% vs 49% and cut normalized cost per successful task by 6.44%.
- Routing stays **100% local and deterministic** — no extra model call, no network hop. Model capabilities are injected from ClawRouter's live catalog at proxy startup, so the product catalog remains authoritative.
- `tool_choice: "none"` is authoritative, and host tool descriptions no longer create false per-turn tool requirements.
- Rollback lever: set `routing.strategy = "rules"`. An optional local-only shadow mode compares the two strategies' metadata without issuing a second paid completion or persisting any prompt.

### Changed — routing engine extracted to router-core

- `src/router/{config,rules,selector,strategy,types}.ts` are gone; `src/router/index.ts` re-exports `@blockrun/router-core`, pinned to immutable commit `6a790eb`. `@blockrun/clawrouter/router` stays available as a subpath export for existing SDK consumers.
- The dependency is **`devDependencies`, deliberately**: tsup's `noExternal` inlines it into `dist/`, so nothing imports it at runtime. Keeping it in `dependencies` would make every `npm install @blockrun/clawrouter` fetch a `codeload.github.com` tarball at install time — resolved from a bare URL with no lockfile-backed integrity, since the published package doesn't ship our lockfile, and broken whenever GitHub is unreachable. The packed tarball still declares zero non-registry dependencies.

### Fixed — a stale build put the retired seed-oss-36b back in three fallback chains

- `dist/router/index.js` shipped `free/seed-oss-36b` in three fallback chains even though router-core's pinned commit had already removed it. The committed artifact had been built against a pre-fix install. Rebuilt.
- This is the same class of bug as v0.12.241: BlockRun **server-redirects** retired free ids, so routing to one **silently defeats `/exclude`** — the caller excludes a model, the router hands it the request anyway, and the gateway answers from the redirect target.
- **A correct commit pin does not imply a correct committed `dist/`.** This repo ships `dist/`, so both have to be checked.

### Added — free-model liveness guard

- `src/router/free-model-liveness.test.ts` walks every tier container's `primary` and `fallback` entries and asserts each `free/*` id is still live. The allowed set is **derived from `src/top-models.json`** (plus the two deliberate `gpt-oss` defaults), so a future free-tier resync updates the guard automatically — models should never be added to it by hand.
- It exists because router selection coverage left this repo along with the code (`selector.test.ts` and `strategy.test.ts` deleted, none added), and the stale-build regression above walked straight through the gap. Verified to fail against that exact regression, and it fails loudly rather than vacuously if the upstream config shape changes.

---

## v0.12.241 — August 7, 2026

Finishes the free-tier resync that #232 started. That commit refreshed the **brand markers** to blockrun's live catalog (71 chat-visible / 6 free) but touched no code, so the router kept routing to a model that has been dead upstream since 2026-08-03.
Expand Down
1 change: 1 addition & 0 deletions dist/cli.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
#!/usr/bin/env node
import '@blockrun/router-core';
1,738 changes: 1,620 additions & 118 deletions dist/cli.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/cli.js.map

Large diffs are not rendered by default.

189 changes: 13 additions & 176 deletions dist/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { RoutingConfig, RoutingDecision } from '@blockrun/router-core';
export { DEFAULT_ROUTING_CONFIG, RouterOptions, RoutingConfig, RoutingDecision, TaskType, Tier, calculateModelCost, filterCandidatesByCapacity, getFallbackChain, getFallbackChainFiltered, inferToolRequirement, route } from '@blockrun/router-core';

/**
* OpenClaw Plugin Types (locally defined)
*
Expand Down Expand Up @@ -355,181 +358,6 @@ type OpenClawPluginDefinition = {
};
};

/**
* Tier → Model Selection
*
* Maps a classification tier to the cheapest capable model.
* Builds RoutingDecision metadata with cost estimates and savings.
*/

type ModelPricing = {
inputPrice: number;
outputPrice: number;
/** Active promo flat price per request (overrides token-based pricing when set) */
flatPrice?: number;
};
/**
* Get the ordered fallback chain for a tier: [primary, ...fallbacks].
*/
declare function getFallbackChain(tier: Tier, tierConfigs: Record<Tier, TierConfig>): string[];
declare function calculateModelCost(model: string, modelPricing: Map<string, ModelPricing>, estimatedInputTokens: number, maxOutputTokens: number, routingProfile?: "free" | "eco" | "auto" | "premium"): {
costEstimate: number;
baselineCost: number;
savings: number;
};
/**
* Get the fallback chain filtered by context length.
* Only returns models that can handle the estimated total context.
*
* @param tier - The tier to get fallback chain for
* @param tierConfigs - Tier configurations
* @param estimatedTotalTokens - Estimated total context (input + output)
* @param getContextWindow - Function to get context window for a model ID
* @returns Filtered list of models that can handle the context
*/
declare function getFallbackChainFiltered(tier: Tier, tierConfigs: Record<Tier, TierConfig>, estimatedTotalTokens: number, getContextWindow: (modelId: string) => number | undefined): string[];

/**
* Smart Router Types
*
* Four classification tiers — REASONING is distinct from COMPLEX because
* reasoning tasks need different models (o3, gemini-pro) than general
* complex tasks (gpt-4o, sonnet-4).
*
* Scoring uses weighted float dimensions with sigmoid confidence calibration.
*/
type Tier = "SIMPLE" | "MEDIUM" | "COMPLEX" | "REASONING";
type RoutingDecision = {
model: string;
tier: Tier;
confidence: number;
method: "rules" | "llm";
reasoning: string;
costEstimate: number;
baselineCost: number;
savings: number;
agenticScore?: number;
/** Which tier configs were used (auto/eco/premium/agentic) — avoids re-derivation in proxy */
tierConfigs?: Record<Tier, TierConfig>;
/** Which routing profile was applied */
profile?: "auto" | "eco" | "premium" | "agentic";
};
type RouterOptions = {
config: RoutingConfig;
modelPricing: Map<string, ModelPricing>;
routingProfile?: "eco" | "auto" | "premium";
hasTools?: boolean;
/** Override current time for promotion window checks (for testing). Default: new Date() */
now?: Date;
};
type TierConfig = {
primary: string;
fallback: string[];
};
type ScoringConfig = {
tokenCountThresholds: {
simple: number;
complex: number;
};
codeKeywords: string[];
reasoningKeywords: string[];
simpleKeywords: string[];
technicalKeywords: string[];
creativeKeywords: string[];
imperativeVerbs: string[];
constraintIndicators: string[];
outputFormatKeywords: string[];
referenceKeywords: string[];
negationKeywords: string[];
domainSpecificKeywords: string[];
agenticTaskKeywords: string[];
dimensionWeights: Record<string, number>;
tierBoundaries: {
simpleMedium: number;
mediumComplex: number;
complexReasoning: number;
};
confidenceSteepness: number;
confidenceThreshold: number;
};
type ClassifierConfig = {
llmModel: string;
llmMaxTokens: number;
llmTemperature: number;
promptTruncationChars: number;
cacheTtlMs: number;
};
type OverridesConfig = {
maxTokensForceComplex: number;
structuredOutputMinTier: Tier;
ambiguousDefaultTier: Tier;
/**
* When enabled, prefer models optimized for agentic workflows.
* Agentic models continue autonomously with multi-step tasks
* instead of stopping and waiting for user input.
*/
agenticMode?: boolean;
};
/**
* Time-windowed promotion that temporarily overrides tier routing.
* Active promotions are auto-applied; expired ones are ignored at runtime.
*/
type Promotion = {
/** Human-readable label (e.g. "GLM-5 Launch Promo") */
name: string;
/** ISO date string, promotion starts (inclusive). e.g. "2026-04-01" */
startDate: string;
/** ISO date string, promotion ends (exclusive). e.g. "2026-04-15" */
endDate: string;
/** Partial tier overrides — merged into the active tier configs (primary/fallback) */
tierOverrides: Partial<Record<Tier, Partial<TierConfig>>>;
/** Which profiles this applies to. Default: all profiles. */
profiles?: Array<"auto" | "eco" | "premium" | "agentic">;
};
type RoutingConfig = {
version: string;
classifier: ClassifierConfig;
scoring: ScoringConfig;
tiers: Record<Tier, TierConfig>;
/**
* Tier configs for agentic mode — models that excel at multi-step tasks.
* Set to `null` to disable agentic tier selection entirely (forces all
* requests through `tiers`, even when tools are present in the request).
*/
agenticTiers?: Record<Tier, TierConfig> | null;
/** Tier configs for eco profile — ultra cost-optimized (blockrun/eco). `null` falls back to `tiers`. */
ecoTiers?: Record<Tier, TierConfig> | null;
/** Tier configs for premium profile — best quality (blockrun/premium). `null` falls back to `tiers`. */
premiumTiers?: Record<Tier, TierConfig> | null;
/** Time-windowed promotions that temporarily override tier routing */
promotions?: Promotion[];
overrides: OverridesConfig;
};

/**
* Default Routing Config
*
* All routing parameters as a TypeScript constant.
* Operators override via openclaw.yaml plugin config.
*
* Scoring uses 15 weighted dimensions with sigmoid confidence calibration.
*/

declare const DEFAULT_ROUTING_CONFIG: RoutingConfig;

/**
* Smart Router Entry Point
*
* Classifies requests and routes to the cheapest capable model.
* Delegates to pluggable RouterStrategy (default: RulesStrategy, <1ms).
*/

/**
* Route a request to the cheapest capable model.
* Delegates to the registered "rules" strategy by default.
*/
declare function route(prompt: string, systemPrompt: string | undefined, maxOutputTokens: number, options: RouterOptions): RoutingDecision;

/**
* Response Cache for LLM Completions
*
Expand Down Expand Up @@ -1000,6 +828,15 @@ type ProxyOptions = {
network: string;
}) => void;
onRouted?: (decision: RoutingDecision) => void;
/** Local comparison only; it never changes the serving request or sends another completion. */
onShadowRouted?: (comparison: {
executed: RoutingDecision;
shadow: RoutingDecision;
sameModel: boolean;
hasTools: boolean;
hasVision: boolean;
requiresStructuredOutput: boolean;
}) => void;
/** Called when balance drops below $1.00 (warning, request still proceeds) */
onLowBalance?: (info: LowBalanceInfo) => void;
/** Called when balance is insufficient for a request (request fails) */
Expand Down Expand Up @@ -1752,4 +1589,4 @@ declare function parseCallArgs(raw: string): {
};
declare const plugin: OpenClawPluginDefinition;

export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, type CachedLLMResponse, type CachedResponse, type CheckResult, DEFAULT_RETRY_CONFIG, DEFAULT_ROUTING_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, type RoutingConfig, type RoutingDecision, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type Tier, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildPartnerTools, buildProviderModels, calculateModelCost, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getFallbackChain, getFallbackChainFiltered, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, resolveModelAlias, resolvePaymentChain, route, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
export { type AggregatedStats, BALANCE_THRESHOLDS, BLOCKRUN_MODELS, type BalanceInfo, BalanceMonitor, type CachedLLMResponse, type CachedResponse, type CheckResult, DEFAULT_RETRY_CONFIG, DEFAULT_SESSION_CONFIG, type DailyStats, type DerivedKeys, EmptyWalletError, FileSpendControlStorage, InMemorySpendControlStorage, InsufficientFundsError, type InsufficientFundsInfo, type LowBalanceInfo, MODEL_ALIASES, OPENCLAW_MODELS, PARTNER_SERVICES, type PartnerServiceDefinition, type PartnerToolDefinition, type PaymentChain, type ProxyHandle, type ProxyOptions, RequestDeduplicator, ResponseCache, type ResponseCacheConfig, type RetryConfig, RpcError, type SessionConfig, type SessionEntry, SessionStore, type SolanaBalanceInfo, SolanaBalanceMonitor, SpendControl, type SpendControlOptions, type SpendControlStorage, type SpendLimits, type SpendRecord, type SpendWindow, type SpendingStatus, type SufficiencyResult, type UsageEntry, VISIBLE_OPENCLAW_MODELS, type WalletConfig, type WalletResolution, blockrunProvider, buildPartnerTools, buildProviderModels, clearStats, plugin as default, deriveAllKeys, deriveEvmKey, deriveSolanaKeyBytes, fetchWithRetry, formatDuration, formatStatsAscii, generateWalletMnemonic, getAgenticModels, getModelContextWindow, getPartnerService, getProxyPort, getSessionId, getStats, hashRequestContent, injectAuthProfile, injectModelsConfig, isAgenticModel, isBalanceError, isBlockrunWebSearchDisabled, isEmptyWalletError, isInsufficientFundsError, isRetryable, isRpcError, isValidMnemonic, loadPaymentChain, logUsage, parseCallArgs, resolveModelAlias, resolvePaymentChain, savePaymentChain, setupSolana, startProxy, syncAgentModelCache };
Loading
Loading