npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pi-smart-router

v1.2.0

Published

Auto-model router middleware for the pi.dev coding agent

Readme

pi-smart-router

Auto-model router middleware for the pi coding agent.

v1.0 — SemVer stable (honest defaults). From the 1.0.0 release on, the public API and documented operator surface follow semantic versioning: breaking changes land only in a new major version, each accompanied by a migration guide — start with docs/migration-v1.md. 1.0 means stable API + honest calibration floors + gates that catch Sept-class failures — not a claim that cheap-tier routing is behaviorally proven. #95 / #110 remain open for verifier-graded recalibration. The published release always mirrors package.json (npm).

pi-smart-router intercepts every LLM inference request and dynamically routes it to the optimal execution engine — balancing cost, capability, latency, and time-to-first-token (TTFT) — without requiring you to manually pick a model for each turn.

| pi-smart-router is | pi-smart-router is not | |--------------------|------------------------| | A pi extension that auto-selects the best model per request | A replacement for pi or your LLM provider | | A three-tier router: local, economical cloud, frontier cloud | A post-generation output judger (FrugalGPT-style) | | Cache-aware with session pinning to preserve prompt-cache economics | A turn-by-turn model switcher that shatters provider caching | | Registry-driven in pi (no YAML copy for normal use) | An RL-trained router requiring agent trace datasets |

How it works

request → hardware probe → loop escalation → turn envelope → context-fit gate
        → low-intensity tier gate → session pin → deterministic triage
        → local zero-tier → triage cloud fallback → HyDRA embedding matcher
        → safe cloud default → context overflow fallback

The pipeline runs 12 stages sequentially with early exit — the moment any stage reaches a routing decision, subsequent stages are skipped. Every decision includes the stage name, reason code, candidates considered, estimated cost, and routing latency for full observability.

| Stage | Budget | What it does | |-------|--------|--------------| | Hardware Probe | — | Checks platform/RAM/battery to gate local inference | | Loop Escalation | — | Detects repeated identical tool failures; escalates session to frontier | | Turn Envelope | <2ms | Classifies turn type: tool_result, planning, subagent, main_loop | | Context-Fit Gate | — | Filters fleet to models whose context window fits estimated input tokens | | Low-Intensity Gate | — | Structural tier hint, cluster match, and P(success) expected-cost scoring | | Session Pin | <1ms | Returns pinned model if session has one; breaks pin on compaction or overflow | | Deterministic Triage | <5ms | Aho-Corasick keyword scan + cyclomatic complexity analysis | | Local Zero-Tier | <15ms | Pings LM Studio + Ollama in parallel; routes locally when eligible | | Triage Cloud Fallback | <2ms | Trivial prompts not claimed locally route to the first healthy economical-cloud model | | HyDRA Matcher | 80-120ms | ONNX embeddings, 3D requirement projection, shortfall gate, multi-objective scoring | | Safe Cloud Default | — | First healthy economical-cloud model (context-fit aware) | | Context Overflow Fallback | — | Escalates to largest-fit model when economical tiers cannot fit |

Research lineage

pi-smart-router builds on ideas from several production and research routing systems:

  • Adopted: GitHub Copilot HyDRA (shortfall matching decoupled from model identities), Zero-Tier local edge-cache pattern, Weave Router session pinning and multi-objective selection
  • Rejected: FrugalGPT sequential cascading (tail latency), RouteLLM matrix factorization (confounder vulnerability), turn-by-turn dynamic routing (cache destruction)

See docs/PRD.md for full architectural justification, docs/deep-research.md for the research survey, docs/routing-roadmap.md for the prioritized quality backlog, docs/gemini-research.md for the second-source agent-router report, and docs/research/README.md for research provenance.

Prerequisites

| Dependency | Required | Notes | |------------|----------|-------| | Node.js >= 22.19.0 | Yes | ES module package; matches package.json engines.node and CI (workflows pin Node 22.19.0) |

Engine floor: package.json declares engines.node >= 22.19.0. If you (or your environment) enable engine-strict=true in .npmrc, installs on Node < 22.19.0 fail with EBADENGINE; on a supported Node (>= 22.19.0), npm ci completes with no EBADENGINE warnings. | pi coding agent | Yes | Extension host | | macOS Apple Silicon | MVP | Primary supported platform | | Linux (x64/arm64) | Experimental | Probe logic supported; not validated on real hardware | | Windows (x64/arm64) | Experimental | Probe logic supported; not validated on real hardware | | LM Studio or Ollama | Optional | Required for zero-tier local routing | | Authenticated cloud providers in pi | Recommended | Anthropic, OpenAI, Google, etc. |

Install

Security: Pi packages run with full system access. Extensions execute arbitrary code. Review source before installing third-party packages (pi packages docs).

Via pi (recommended)

Install from npm / pi.dev/packages:

pi install npm:pi-smart-router
pi --list-models | grep smart-router

Project-local install (writes to .pi/settings.json):

pi install -l npm:pi-smart-router

Then in pi:

/model smart-router/auto
/smart-router status

First run: pi install runs npm install for package dependencies (better-sqlite3 compiles natively). The first routed request downloads HyDRA ONNX weights to .pi-smart-router/models/ under your state directory.

Via npm (library API)

npm install pi-smart-router

Use createRouter() / createRouterFromFleet() for programmatic integration without the pi extension. That path is the catalog / routing-core composition root (not full extension wiring). See Library vs extension and Optional: YAML fleet (library API).

From source (contributors)

git clone https://github.com/beettlle/pi-smart-router.git
cd pi-smart-router
npm install

The repo ships a project-local pi extension at .pi/extensions/smart-router/. pi auto-discovers it when you run pi from the repo root (after the project is trusted — see Develop from clone).

Quick start

After installing via pi install npm:pi-smart-router (or from clone — see below):

  1. Authenticate providers (/login) and enable models in your scoped list if you use one (/scoped-models)
  2. /model smart-router/auto — every turn runs through the routing pipeline
  3. /smart-router status, /smart-router history, or /smart-router stats — inspect routing decisions and window aggregates

Set SMART_ROUTER_LOG_ROUTING=1 before starting pi to print each routing decision to stderr (see Environment variables).

Use with pi

Detailed steps for the operator path above.

Installed via npm

Global install (pi install npm:pi-smart-router) registers the extension from ~/.pi/agent/settings.json. No project /trust prompt is required for npm-installed extensions — start pi from any directory.

After auth or model list changes, restart pi or run /reload.

Develop from clone

Requires Pi ≥ 0.85.1 (pi.minPiVersion). The extension loads TypeScript from this repo via pi’s loader — no npm run build needed for dogfooding.

Recommended (works from any cwd): install the clone as a path package and remove any published npm copy. npm and path packages have different identities; leaving both installed can double-load and you may keep running a stale npm tarball.

# From anywhere — remove published package if present
pi remove npm:pi-smart-router

# Prefer an absolute path (relative paths are resolved from the install cwd first).
# Pi stores a path relative to ~/.pi/agent/ in settings (same pattern as other local packages).
pi install /path/to/pi-smart-router

# Pi does not run npm install for path packages
cd /path/to/pi-smart-router && npm install

pi list   # must show this clone path, not ~/.pi/agent/npm/.../pi-smart-router
pi --list-models | grep smart-router

Alternative (repo-root discovery only): with the npm package removed, start pi with cwd at this repo root. Project-local extensions under .pi/extensions/ load only after the project is trusted — without trust, smart-router never appears in /scoped-models or /model.

On first run, pi prompts you to trust the project when it detects .pi/extensions/. Accept the prompt.

Later or missed prompt: run /trust inside pi to save a trust decision for this directory (or its parent) to ~/.pi/agent/trust.json. Trust on a parent folder (for example ~/Documents/github) applies to this repo as well. After /trust, restart pi — the current session is not reloaded automatically.

Verify the extension loaded:

pi --list-models | grep smart-router

You should see smart-router auto. If the line is missing:

  1. Confirm pi list points at this clone (path package) or that cwd is the repo root (discovery mode).
  2. Confirm no npm:pi-smart-router entry remains in settings while developing from clone.
  3. Confirm the project is trusted if relying on .pi/extensions/ discovery (/trust, or check ~/.pi/agent/trust.json).
  4. Restart pi or run /reload after trusting or changing packages.

Non-interactive one-shot checks can pass --approve to trust project-local resources for that run only.

Select the auto model

Switch to the smart-router provider (from any directory after npm install, or from repo root when developing from clone):

/model smart-router/auto

If you use scoped models (/scoped-models or enabledModels in settings), enable smart-router/auto there first — when a scoped list is active, /model only resolves models in that list.

This registers smart-router as a custom provider with a single auto model. Every inference request runs through the routing pipeline and delegates to the selected underlying provider's streaming API.

cursor/auto vs smart-router/auto

pi exposes two different auto models. They are easy to confuse but play different roles:

| Model | Provider | Role | |-------|----------|------| | smart-router/auto | smart-router (this extension) | Runs the routing pipeline on every turn and delegates to whichever underlying model HyDRA selects | | cursor/auto | cursor (pi registry) | Cursor's opaque auto model — direct inference target when selected; Cursor picks the backend model |

Recommended dogfood setup: use /model smart-router/auto so routing, pinning, and telemetry stay active. Enable cursor/auto (and other Cursor models such as composer-latest) in your scoped fleet so the router can select them when appropriate — for example on planning turns or when the Gemini tool-history guard excludes unrepairable Google replay state.

When to pin /model cursor/auto directly (bypass the router):

  • You want Cursor's opaque auto selection on every turn with no routing overhead
  • You are debugging Cursor SDK auth or delegation outside the router
  • You need a stable, non-routed session for comparison with routed behavior

When to use smart-router/auto:

  • You want cost/capability-aware model selection across your full authenticated fleet
  • You rely on session pinning, failover, or /smart-router status / history / stats telemetry
  • Tool-heavy sessions with Gemini economical models work via in-repo replay repair (cross-provider included); the tool-history guard reroutes to non-Google models such as cursor/auto for unrepairable replay state (see pi-smart-router#85, pi-smart-router#158)

Cursor models (cursor/*, composer-*, and the opaque fleet id default) map to frontier-cloud tier in pi-model-mapper.ts so HyDRA can score them against Gemini and Claude instead of treating them as unknown economical models (pi-smart-router#40, pi-smart-router#70). Related: pi-smart-router#23 (turn envelope / pin order), pi-smart-router#37 (Gemini thought_signature errors).

Cursor subscription quota vs API cost

Cursor models bill against your Cursor Pro subscription quota, not per-token API rates. The mapper sets fallback_cost_per_1m: 0 (no API billing) and a separate quota_cost_per_1m virtual rate used only for frugality scoring and telemetry (SP-096). Economical API models (e.g. gemini-flash-lite) can outscore composer-latest on routine main_loop turns when capabilities are sufficient.

Quota-sensitive fleet hygiene: if you are near Cursor usage limits, exclude composer-latest (and other heavy Cursor frontier models) from your pi scoped fleet enable-list. Leave economical API models enabled so turn envelope and HyDRA prefer paid API tiers over subscription quota. The opaque id default is mapped to frontier tier — do not rely on it as an economical fallback.

Operator commands

| Command | Purpose | |---------|---------| | /smart-router | Same as status (default when no subcommand is given) | | /smart-router status | Show fleet mode, fleet size, pricing freshness/staleness, and the last routing decision (stage, tier, selected model, latency) | | /smart-router history | Show recent routing telemetry from SQLite (default limit; optional numeric limit, e.g. /smart-router history 20). Displays the concrete delegated model id (never bare virtual auto) | | /smart-router stats | Privacy-safe session/window aggregates from routing telemetry: count, mean cost/latency with cost_basis labeling (actual vs estimated, SP-241), planning_delegate vs direct share, local vs cloud when distinguishable, and role cost breakdown. Optional vs-always-frontier savings when frontier fleet prices exist (omitted otherwise). The JSON snapshot (buildStatsSnapshot, automation surface) additionally carries warm rolling cost_calibration actual/estimate buckets (SP-242). Optional numeric limit, e.g. /smart-router stats 50 | | /smart-router mode scoped | Route only among pi's enabled model patterns (default) | | /smart-router mode all | Route among all authenticated models in the registry | | /smart-router pricing refresh | Manually fetch LiteLLM pricing from LITELLM_PRICING_URL, persist to SQLite, and rebuild the fleet with updated rates | | /smart-router export dataset [--limit N] | Export opt-in routing dataset as JSONL (requires SMART_ROUTER_DATASET=1) | | /smart-router export telemetry-contrib [--limit N] [--embeddings] | Export privacy-safe community telemetry JSON for calibration contributions (--embeddings opts in to captured 384-dim embedding rows) | | /smart-router feedback good\|bad | Label the last auto-routed request outcome (requires SMART_ROUTER_DATASET=1) | | /smart-router unpin | Clear the current session pin (in-memory and SQLite) so the next request runs the full routing pipeline | | /smart-router plan [--json] | Read-only local placement report: encoder resident status, local model warm/cold, RAM/disk constraints, cold vs warm TPS, and bottleneck guess. --json prints the schema-stable report for automation | | /smart-router doctor | Read-only local readiness checklist (✓/✗) with bottleneck guess and recommendation — validate placement without starting a route |

Fleet mode persists in the session. Use scoped to respect your /model enable-list; use all when you want the router to consider every provider you have logged into.

Local placement plan / doctor (read-only, #116)

/smart-router plan and /smart-router doctor report local placement readiness without mutating anything — no route, pin, or gate is touched. Inspired by Colibrì's coli plan / coli doctor.

The report covers:

  • Encoder — whether HyDRA ONNX artifacts are resident in the cache (.pi-smart-router/models/) or will download on first route
  • Local model warm/cold — LM Studio / Ollama reachability and whether a model is actually loaded (warm) vs reachable-but-unloaded (cold-start expected)
  • Hardware — platform/arch, total/free RAM, hardware-probe verdict (full_local / classification_only / disabled), battery state
  • Disk — free GiB; flagged constrained below 2 GiB
  • Throughput (cold vs warm TPS) — see formula below
  • Bottleneck guess — one of none, unsupported-platform, battery, memory, disk, no-local-runtime, cold-start, cold-throughput, warm-throughput, with a rationale string
  • Recommendationlocal-ready, local-warmup-needed, or local-unavailable

/smart-router plan --json prints the same report as schema-stable JSON (schemaVersion: 1, kind: "smart-router-placement-plan"; all keys always present, unknowns are null) for automation.

Cold vs warm TPS formula. Throughput samples are tagged by phase: warm samples measure steady-state generation after model load; cold samples include cold-start load cost and never count toward viability. warmMedianTps = median(tps where phase='warm'); viability is warmSamples > 0 AND warmMedianTps >= threshold (default 25 tok/s). Cold-only windows fail closed: when only cold samples exist, local viability is false by policy (requireWarmSamples: true) — cold-start cost must not masquerade as steady-state throughput.

Quality-preserving resource policy. Under RAM/disk/battery pressure the router prefers "local unavailable / escalate safely" to a cloud default over silently weakening encoder fidelity (no quantization flips, no cheaper cascades, no FrugalGPT-style downgrade chains). plan/doctor surface this policy verbatim in the report's policy block.

After typing /smart-router (with a trailing space), press TAB to see subcommands. Continue TAB-completing after mode or pricing for sub-options (scoped/all, refresh).

5. Verify

npm run verify:ci

Concurrency contract

RouterPipeline.route() calls on a single router instance are single-flight: concurrent calls are serialized internally (SP-230, #141). Each exclusive route owns one per-route RoutingContext; overlapping executions are queued rather than interleaved — each queued call waits at most one routing latency. This applies to createRouter() / createRouterFromFleet() handles: a shared router.dispatch is safe to call concurrently, and serialization does not change routing policy outcomes. For parallel routing throughput, create separate router instances.

Library vs extension

pi-smart-router ships in two shapes, and they are not feature-identical (#153):

| Path | What you get | |------|--------------| | Pi extension (pi install npm:pi-smart-router, or project-local .pi/extensions/smart-router/ when developing from clone) — supported product composition root | The full product — the routing pipeline plus the stream-level behaviors below, with hardware probe + store-backed telemetry wired via createDispatchOptions() in .pi/extensions/smart-router/fleet-bootstrap.ts | | npm library (createRouter() / createRouterFromFleet() / GatewayDispatch) — catalog / embedder composition root | The routing core — the 12-stage pipeline, fleet mapping, gateway health/failover selection, and constructor defaults for costEstimator + localRuntime only. Hardware probe stays disabled and telemetry is opt-in unless you pass those ports. Stream-level behaviors are stubbed or left to your embedder loop |

Extension-only capabilities

These behaviors run in .pi/extensions/smart-router/ and have no equivalent in the library API:

| Capability | Extension implementation | Library status | |------------|--------------------------|----------------| | Planning delegate spawn — cache-preserving ephemeral frontier sub-call on planning turns, with observation injection and bounded timeouts (SP-144, SP-213 / #71, #120) | .pi/extensions/smart-router/planning-delegate.ts — compressed-context sub-call via streamSimple; falls back to direct frontier with a documented fallback_reason | The pipeline still emits planning_delegate decisions with delegate model and compressed limits, but nothing spawns the delegate — an embedder must implement the sub-call itself or accept direct-frontier routing | | Stream failover loop — live provider-error failover across candidate models with user-facing notices (atomic state machine, #33) | .pi/extensions/smart-router/route-and-delegate.ts (~L377–595) — retries stream delegation across alternates, emits failover notices, ends in SP-226 fail-open safe default | GatewayDispatch.selectFailover() only selects an alternate model; no stream retry loop runs. The embedder owns iterating over failures and re-dispatching | | Output headroom escalation — exclude failover candidates whose context window cannot fit input plus the required output floor (SP-108) | route-and-delegate.ts + .pi/extensions/smart-router/delegation-runtime.ts — per-attempt computeOutputHeadroom checks; headroomExcludedModelIds accumulate across the failover loop | src/domain/delegation/output-headroom.ts ships the helper, but no library caller wires it into GatewayDispatch.dispatch() — the embedder must apply it per attempt | | Cursor quota handling — subscription-quota exhaustion detection and failover to cursor/auto or economical API models with cursor_quota_exhausted telemetry (SP-097 / #70) | The extension stream loop catches quota errors from live streams and drives selectFailover reactively | Detection and failover selection exist in src/infrastructure/gateway/gateway-dispatch.ts (isCursorQuotaExhaustedError, per-model quota tracking), but the reactive trigger lives in the extension's stream loop — library dispatch alone does not observe provider stream errors |

The middleware is a lifecycle stub, not a router

createPiRouterMiddleware() / RouterHandle.register() (src/api/middleware/pi-router-middleware.ts) registers lifecycle hooks only — compaction flags and model_select overrides consumed when building the next routing request. It does not intercept LLM streams, route requests, or delegate inference. The production stream path lives in the pi extension (route-and-delegate.ts, stream-delegation.ts, delegate-stream.ts), not in the npm-exported middleware. Treat middleware as a flag registrar; routing happens through your call to router.dispatch.dispatch() or the extension's stream path.

Recommended integration path

  • pi users: install the extension (pi install npm:pi-smart-router). It is the supported composition root — everything in the table above works out of the box, including failover, delegate spawn, headroom escalation, quota reaction, hardware probe, and store-backed routing telemetry.
  • npm embedders: you get the routing core (12-stage pipeline, fleet mapping, gateway health tracking, failover selection). Bare createRouter() does not wire hardware probe or telemetry by default — pass GatewayDispatchOptions on createRouterFromFleet(fleet, options) when you need those ports. Plan to implement your own stream delegation, failover iteration, headroom checks, and planning-delegate spawn around the decisions the pipeline returns — or track #149 (extension public facade), the migration plan for exposing the extension's stream/delegation surface as supported library API so this gap closes over time. Until #149 lands, the extension modules also import src/** internals directly, so deep imports into src/ are not a stable API. See docs/extension-package-boundary.md for the facade vs internal-API boundary, the deep-import lint guard, and the extension coverage gate. See also Composition roots in the 1.0 migration guide.
pi extension path (full product)        npm library path (routing core)
────────────────────────────────        ───────────────────────────────
pi (host agent)                         your host application
  └─ .pi/extensions/smart-router/         └─ createRouter() / createRouterFromFleet()
       ├─ routing pipeline (src/)  ════════    ├─ routing pipeline (src/)        ← shared core
       ├─ stream failover loop          ✗      ├─ GatewayDispatch: health tracking,
       ├─ planning delegate spawn       ✗      │   failover selection only
       ├─ output headroom escalation    ✗      ├─ lifecycle middleware (stub: hooks only)
       └─ cursor quota failover         ✗      └─ embedder implements: stream loop,
                                            delegate spawn, headroom checks, quota reaction

= capability exists only on the extension path today; #149 is the plan to close the gap.

Fleet behavior

When you use smart-router/auto, the extension does not read config/models.yaml. Instead:

  1. DiscovermodelRegistry.getAvailable() returns authenticated models from pi.
  2. Scope — In scoped mode, filter to patterns from pi settings (getEnabledModels()). In all mode, use the full registry.
  3. Mapsrc/config/pi-model-mapper.ts maps each pi model to a ModelProfile (tier, capabilities, pricing) using provider and model-id patterns.
  4. RoutecreateRouterFromFleet() runs the 12-stage pipeline on each request.
  5. Delegate — The extension resolves the chosen model in the registry and forwards the stream via pi-ai's built-in provider APIs.

Unknown models receive conservative economical-cloud defaults. Local providers (lmstudio, ollama) map to zero-tier. Cursor provider models (cursor/*, composer-*, opaque id default) map to frontier-cloud with explicit capability defaults (SP-086, SP-098). Benchmark-grounded capability vectors (including multi-fleet github-copilot/*, Gemini, and Anthropic dogfood IDs, aliased family-by-family) are documented in the capability profile coverage report (#108 / #124).

To refresh after auth or settings changes, restart pi or /reload extensions.

Optional: YAML fleet (library API)

For programmatic integration without the pi extension, load a static fleet catalog from YAML and route via GatewayDispatch.dispatch().

This path is catalog-oriented: createRouter() loads models.yaml and constructs GatewayDispatch with library defaults (costEstimator + localRuntime). It does not call the extension's createDispatchOptions() — so hardware probe remains disabled and no store-backed telemetryEmitter is attached unless you pass those options yourself. Prefer the pi extension when you want the full product composition root.

cp config/models.yaml.example ./config/models.yaml
# Edit config/models.yaml — at least one model per tier
import { createRouter, createRouterFromFleet } from 'pi-smart-router';

const router = createRouter({ modelsPath: './config/models.yaml' });
router.register(piExtensionHooks); // lifecycle only: compaction + model override

const decision = await router.dispatch.dispatch(routingRequest);
// Embedder forwards inference to decision.selected_model_id

// Optional: wire probe/telemetry yourself (same options bag as GatewayDispatch)
// createRouterFromFleet(fleet, { systemInfoProvider, hardwareConfig, telemetryEmitter, ... })

Embedder integration paths

| Path | When to use | Routing | Lifecycle hooks | |------|-------------|---------|-----------------| | Pi extension (recommended) | Running inside pi | pi install npm:pi-smart-router (or project-local .pi/extensions/smart-router/ when developing from clone) registers smart-router/auto and delegates streams | Extension calls router.register(); compaction/model overrides wired automatically | | Library API | Custom host, tests, or non-pi embedders | Your code calls router.dispatch.dispatch() (or wraps the pipeline) | Call router.register(hooks) to wire compaction and model_select events |

The library createPiRouterMiddleware() / RouterHandle.register() registers lifecycle hooks only — not routing, context capture, or before_provider_request. Do not expect middleware to intercept LLM streams; that is the extension's streamSimple path or your embedder's dispatch loop.

createRouter() returns a RouterHandle:

| Property | Type | Purpose | |----------|------|---------| | middleware | PiRouterMiddleware | Lifecycle hook registrar (register, lifecycleHookState) | | dispatch | GatewayDispatch | Gateway with circuit breaker, failover, rate limiting | | fleet | readonly ModelProfile[] | Loaded fleet catalog | | register | (hooks) => void | Alias for middleware.register — attach pi lifecycle hooks |

You can also pass a pre-built fleet:

import { createRouterFromFleet } from 'pi-smart-router';

const router = createRouterFromFleet(myFleetProfiles);

Example fleet entry:

models:
  - id: local-gemma-4-7b
    tier: zero-tier
    provider: lmstudio
    endpoint: http://localhost:1234/v1
    capabilities:
      reasoning: 0.3
      code_gen: 0.6
      tool_use: 0.1
    pricing:
      registry_key: local/free
      fallback_cost_per_1m: 0.0

  - id: claude-3.5-haiku
    tier: economical-cloud
    provider: anthropic
    # ...

  - id: claude-3.5-sonnet
    tier: frontier-cloud
    provider: anthropic
    # ...

Tiers: zero-tier, economical-cloud, frontier-cloud. See config/models.yaml.example.

Routing cluster catalog (library API)

Reference prompts grouped by tier bias for semantic cluster matching (SP-099). Operators tune clusters in YAML without code changes. Precomputed centroids live in config/routing-centroids.json (SP-114); when that file is absent, centroids are computed at load time as the mean embedding of each cluster's reference prompts.

cp config/routing-clusters.yaml.example ./config/routing-clusters.yaml
cp config/routing-centroids.json.example ./config/routing-centroids.json
# Edit reference_prompts, min_similarity, and min_margin per cluster
# Regenerate centroids after catalog changes:
npm run routing:bootstrap-centroids

The bootstrap script embeds each reference prompt via the HyDRA MiniLM ONNX pipeline (384-dim), mean-pools to centroid vectors, and writes config/routing-centroids.json with { cluster_id, tier_bias, centroid, reference_count } per cluster. ONNX artifacts cache under .pi-smart-router/models/ on first run.

import { loadRoutingClusters } from 'pi-smart-router';

const catalog = await loadRoutingClusters({
  filePath: './config/routing-clusters.yaml',
  embedder: myTextEmbedder, // shared ONNX embedder (SP-100)
});
// Reason codes: cluster_${id} — e.g. cluster_low_stakes_general

// createClusterMatcher (cluster-matcher module) prefers routing-centroids.json when present.

Cluster IDs are stable reason-code prefixes (cluster_low_stakes_general, cluster_architecture, etc.). See config/routing-clusters.yaml.example.

Configuration

Environment variables

| Variable | Default | Purpose | |----------|---------|---------| | ROUTER_STATE_DB_PATH | ./.pi-smart-router/state.db | Override SQLite state store location (telemetry, pricing catalog, session data) | | SMART_ROUTER_LOG_ROUTING | (unset) | Set to 1 to log each routing decision to stderr as JSON (debugging dogfood sessions). Canonical payload builder (buildRoutingDecisionLogPayload) includes top-level stage, reason_code, low_intensity_score, tier_hint, local_eligible_reason, and cluster_id (plus nested cluster_summary / features). The pi extension’s live stderr logger is still a slim subset — see LOG_ROUTING field checklist | | SMART_ROUTER_DATASET | (unset) | Set to 1 to opt in to privacy-safe routing dataset capture (metadata and feature fields only; 30-day / 10k-row retention). Prompt text, messages, and tool arguments are never stored. Required for outcome labels and P(success) training export. See #8. | | SMART_ROUTER_DATASET_FINGERPRINT | (unset) | Set to 1 (requires SMART_ROUTER_DATASET=1) to store an install-local HMAC-SHA256 fingerprint of each normalized prompt for duplicate detection within this install. The install pepper lives in .pi-smart-router/.dataset-key (gitignored) and is never exported. Warning: short or common prompts are vulnerable to offline rainbow-table guessing; use only when you accept that tradeoff. See #10. | | SMART_ROUTER_DATASET_EMBEDDINGS | (unset) | Set to 1 (requires SMART_ROUTER_DATASET=1) to capture the raw 384-dim HyDRA encoder embedding on each dataset row (derived dense vector of the metadata-prefixed routing input — never prompt text). Embeddings stay local until you additionally export with --embeddings; they exist so the hydra_projection ≥100-row training floor can ever be met. See #170. | | MODELS_YAML_PATH | ./config/models.yaml | Fleet catalog path (library API only) | | SMART_ROUTER_PLANNING_TURN_BUFFER | 2 | SAAR planning buffer: frontier planning turns allowed before hard-lock (v0.2.0 Continuity) | | SMART_ROUTER_PLANNING_DELEGATE_ENABLED | true | Enable cache-preserving planning delegate (#71) | | SMART_ROUTER_PLANNING_DELEGATE_MAX_MESSAGES | 12 | Compressed-context message cap for frontier sub-call | | SMART_ROUTER_PLANNING_DELEGATE_MAX_TOKENS | 16384 | Compressed-context token cap for frontier sub-call | | SMART_ROUTER_PLANNING_DELEGATE_EXCLUDE_EXECUTION_HISTORY | true | Exclude tool execution history from delegate payload | | SMART_ROUTER_PLANNING_DELEGATE_GLOBAL_TIMEOUT_MS | 120000 | Global cap (ms) on the whole planning-delegate stage per planning turn — bounds fan-out wall-clock so a stalled worker cannot hang TTFT (#120) | | SMART_ROUTER_PLANNING_DELEGATE_SUB_CALL_TIMEOUT_MS | 30000 | Per-call cap (ms) on each delegate sub-call worker; on expiry the worker is cancelled/abandoned and routing falls back to direct frontier with planning_delegate_timeout (#120) | | SMART_ROUTER_ADAPTIVE_REASONING_ENABLED | true | Master switch for the adaptive thinking-level policy (#166); false passes the session thinking level through unchanged | | SMART_ROUTER_ADAPTIVE_REASONING_MIN_LEVEL | (unset) | Floor on policy-derived thinking levels (minimal\|low\|medium\|high\|xhigh\|max) — see Adaptive reasoning | | SMART_ROUTER_ADAPTIVE_REASONING_MAX_LEVEL | (unset) | Ceiling on policy-derived thinking levels (incl. turn-class upgrades) — see Adaptive reasoning | | SMART_ROUTER_PREFIX_CACHE_WEIGHT | 0.20 | SAAR weight on warm prefix value in cache breakeven math (0–1; #73) | | SMART_ROUTER_IDLE_TIMEOUT_SECONDS | 300 | SAAR idle seconds before pin reopens for full re-route | | SMART_ROUTER_SWITCH_THRESHOLD | 0.5 | SAAR switch score gate (0–1) for tier upgrades during hard-lock | | ROUTER_SAFE_DEFAULT_TIER | economical-cloud | Fallback tier on any routing failure | | LITELLM_PRICING_URL | — | LiteLLM pricing JSON source |

LOG_ROUTING field checklist

When SMART_ROUTER_LOG_ROUTING=1, prefer the canonical payload from buildRoutingDecisionLogPayload (library / tests). Checklist for #99:

| Field | In payload builder? | Notes | |-------|---------------------|-------| | stage | Yes (top-level) | Pipeline stage that decided | | reason_code | Yes (top-level) | Machine-readable reason | | low_intensity_score | Yes (top-level + cluster_summary) | Null when low-intensity stage did not run | | tier_hint | Yes (top-level + cluster_summary) | Null when no tier hint | | local_eligible_reason | Yes (top-level + features) | Null when local_zero did not evaluate eligibility | | cluster_id | Yes (top-level + cluster_summary) | Null when no cluster match | | pricing_window | Yes (top-level + peak_pricing_summary) | Peak vs off-peak rationale for the selected model (SP-244 / #165); the extension stderr logger carries pricing_window + peak_pricing |

Gap: the pi extension’s live stderr path (logRoutingDecision in .pi/extensions/smart-router) still emits a slim JSON object (selected_model_id, stage, reason_code, features, delegate) and does not yet call buildRoutingDecisionLogPayload. SQLite /smart-router history and the payload builder carry the full checklist; wire the extension logger in a follow-up if dogfood needs identical stderr shape.

History model id: /smart-router history resolves bare/smart-router virtual auto to the concrete planning-delegate primary (or qualifies Cursor opaque auto as cursor/auto) so operators see the delegated fleet model, not the virtual router id.

SAAR session pin and cache breakeven (v0.2.0 Continuity)

v0.2.0 adds Session-Aware Agentic Routing (SAAR) pin knobs (#72) and a cache breakeven gate (#73) that blocks tier switches when marginal_savings + future_cache_value <= cache_reprime_cost — preventing cheap-turn savings from invalidating a warm prefix cache.

| Knob | Env var | Default | Effect | |------|---------|---------|--------| | Planning buffer | SMART_ROUTER_PLANNING_TURN_BUFFER | 2 | First N turns may route planning to frontier while pin metadata stays economical | | Prefix cache weight | SMART_ROUTER_PREFIX_CACHE_WEIGHT | 0.20 | Discounted future cache credit in breakeven | | Idle reopen | SMART_ROUTER_IDLE_TIMEOUT_SECONDS | 300 | Seconds of inactivity before SAAR resets and pin reopens | | Hard-lock upgrade gate | SMART_ROUTER_SWITCH_THRESHOLD | 0.5 | Score threshold for tier upgrades after buffer exhaust |

Dogfood verification (multi-turn planning session)

  1. Start pi with routing logs: SMART_ROUTER_LOG_ROUTING=1 pi (optional: tune SAAR env vars above).
  2. Run /model smart-router/auto and begin a multi-turn planning session (planning turns mixed with tool results).
  3. Inspect stderr JSON lines — confirm saar_summary.buffer_active / saar_reason_code: saar_buffer_active on early planning turns, then hard_lock: true / saar_hard_lock after the buffer exhausts.
  4. On a warm pinned session, trigger a tool_result sub-route — when breakeven fails, expect breakeven_summary.decision: "blocked" and breakeven_reason_code: breakeven_blocked while the pin holds.
  5. Use pi router explain (or POST /v1/route/explain) on the same session — features.breakeven and features.saar mirror telemetry fields for operator audit.

See routing-roadmap.md §2 P0 for design context.

Long-running pi sessions — in-memory state eviction (v0.21.0, #145). Session pins and in-memory routing snapshots live in process memory, not in the SQLite telemetry store. When pi ends a session, the extension's session_shutdown handler (reason: quit / reload / new / resume / fork) calls evictInMemorySessionState (src/api/session-eviction.ts) and drops all in-memory routing state for that session — pins, cache-breakeven snapshots, turn metadata. A new session starts cold: no stale pin, no warm-cache assumption carries over. Persistent telemetry in .pi-smart-router/state.db is untouched. As a safety net for orphaned sessions (e.g. a crashed pi process that never emitted session_shutdown), a sweep on session_start evicts sessions idle longer than ORPHAN_SESSION_TTL_MS (24 hours, exported from .pi/extensions/smart-router/session-lifecycle.ts); the sweep fails open — a missing session id or sweep error never blocks session start.

Planning delegate (v0.4.0 Delegate)

When a planning turn would route primary inference to frontier while a warm economical session pin is active, smart-router prefers cache-preserving delegation (#71):

  1. Pipeline (turn_envelope) emits planning_delegate — primary stays on the pinned economical model; features.planning_delegate names the frontier delegate model and compressed-context limits.
  2. Pi extension (.pi/extensions/smart-router) runs an ephemeral frontier sub-call with compressed context (tool execution history excluded by default), injects the result as an observation user message, then delegates primary streaming to the pinned economical model.
  3. Fallback — when delegate is disabled, spawn fails, or the delegate model is missing from the registry, the extension falls back to a direct frontier route with a documented fallback_reason in explain/telemetry.

Stream piping (SP-170): Primary delegated inference live-forwards provider events to pi (start / text_delta / … as they arrive). The planning-delegate sub-call stays buffered — only the final observation text is injected into primary context; frontier tokens from the ephemeral sub-call are discarded and never reach the user-facing stream. On infra failover, a synthetic text_delta notice is pushed after the retry stream's start (no mutation of a buffered event array).

| Knob | Env var | Default | Effect | |------|---------|---------|--------| | Delegate enabled | SMART_ROUTER_PLANNING_DELEGATE_ENABLED | true | When false, SAAR buffer allows direct frontier planning (planning_direct_frontier + planning_delegate_disabled) | | Compressed message cap | SMART_ROUTER_PLANNING_DELEGATE_MAX_MESSAGES | 12 | Max messages sent to the frontier sub-call | | Compressed token cap | SMART_ROUTER_PLANNING_DELEGATE_MAX_TOKENS | 16384 | Token budget for compressed delegate context | | Exclude tool history | SMART_ROUTER_PLANNING_DELEGATE_EXCLUDE_EXECUTION_HISTORY | true | Strip tool-call / tool-result turns from delegate payload |

Coordination boundary with pi core: smart-router owns routing (when to delegate, which models, compressed limits, fallback reason codes). Sub-agent spawn and observation injection run in the pi extension via streamSimple — pi core must expose a delegate/stream API the extension can call; smart-router does not orchestrate pi's outer sub-agent scheduler. Operators enabling /model smart-router/auto get delegate behavior automatically when the extension is loaded; no separate pi sub-agent config is required beyond a frontier model in the registry.

Dogfood verification (planning delegate)

  1. Start pi with routing logs: SMART_ROUTER_LOG_ROUTING=1 pi and /model smart-router/auto.
  2. Begin a session on an economical pin (routine prompts), then trigger planning turns (e.g. architecture or multi-step design work).
  3. Inspect stderr JSON — on delegate turns expect reason_code: planning_delegate, planning_delegate_summary.path: "delegate", primary_model_id equal to the pin, and delegate_model_id pointing at frontier.
  4. Confirm primary inference stays on the economical model (cache-friendly) while stderr shows [smart-router] planning delegate sub-call completed with the frontier model id.
  5. Disable delegate (SMART_ROUTER_PLANNING_DELEGATE_ENABLED=false) and repeat — expect planning_direct_frontier with fallback_reason: planning_delegate_disabled.
  6. Use pi router explain (or POST /v1/route/explain) on the same session — features.planning_delegate mirrors live routing (path: delegate vs direct, fallback_reason when applicable).

See routing-roadmap.md §2 P0 and GitHub #71 for acceptance criteria.

Adaptive reasoning (thinking level) (#166)

Adaptive reasoning tunes the thinking intensity of the model already selected — it never changes which model runs. Per turn class:

| Turn class | Policy level | |-----------|--------------| | tool_result | minimal | | main_loop | low | | planning / planning_delegate | medium | | frontier escalation / loop_escalation | high |

pi passes the session thinking level on every call; the router treats pi's ambient default (medium) as adjustable by policy, and any other explicit level as an operator /thinking floor that is never lowered (a turn-class upgrade may still raise it). Chatty profiles (high verbosity_factor) additionally get a one-line conciseness nudge at minimal/low.

Three knobs that sound alike, do different things:

| Knob | Acts on | What it changes | |------|---------|-----------------| | Adaptive reasoning (adaptive_reasoning.*) | The delegated call's reasoning option | Thinking intensity of the already-selected model — cost of thinking, not model choice | | frugality.lambda_verbosity | Multi-objective selection scoring | Which model gets picked — penalizes verbose models while ranking candidates; never touches the delegated call's reasoning option | | /thinking (pi session command) | The caller-provided reasoning level | Explicit operator override — an explicit level is never lowered by policy or bounds |

Operator knobs (config adaptive_reasoning / env):

| Key | Env var | Default | Effect | |-----|---------|---------|--------| | enabled | SMART_ROUTER_ADAPTIVE_REASONING_ENABLED | true | When false, the policy is skipped — delegated calls pass the session thinking level through unchanged (reasoning_reason_code: adaptive_reasoning_disabled) | | min_level | SMART_ROUTER_ADAPTIVE_REASONING_MIN_LEVEL | (none) | Floor: policy-derived levels are raised to at least this level. Discrete level (minimal\|low\|medium\|high\|xhigh\|max) — deliberately not a free-form verbosity percent | | max_level | SMART_ROUTER_ADAPTIVE_REASONING_MAX_LEVEL | (none) | Ceiling: policy-derived levels (incl. turn-class upgrades) are capped at this level. When min_level exceeds max_level (e.g. via env), the ceiling wins (cost-safe) |

Floor/ceiling bind only what the policy itself derives. An explicit operator /thinking choice is never lowered by either bound (a floor can still raise one via the policy-upgrade path). Both bounds re-clamp down to the model's supported levels.

Fail-open behavior: models that do not support reasoning options (reasoning: false, or a thinkingLevelMap mapping every relevant level to null) pass caller options through unchanged — telemetry records reasoning_reason_code: reasoning_unsupported and the route never fails. Providers that ignore reasoning options degrade to a no-op.

Telemetry: each routed delegation records reasoning_level_requested (the session/caller level), reasoning_level_applied (the effective delegated level), and reasoning_reason_code (e.g. turn_envelope_main_loop, operator_thinking_floor, operator_floor_applied, operator_ceiling_applied, reasoning_unsupported) on the routing telemetry row — enriched post-delegation like usage actuals (SP-241). Inspect via /smart-router history.

Degraded neural failover sandwich (#119)

When the encoder/neural stage (HyDRA) fails, is misconfigured, or exceeds its latency budget without a selection, routing fails open through a cheap chain instead of crashing the host agent (#119):

  1. learned — optional privacy-safe map keyed by requirement fingerprint (SHA-256 of the rounded requirement vector) or cluster id → preferred tier. Raw prompt text is never stored. Exact-key policy: fingerprint match first, cluster id second (no fuzzy matching). Writes are validated (bounded floats, snake_case cluster ids, tier enum) and capped (FIFO eviction) so confounder attacks cannot poison routing memory.
  2. heuristic — optional operator pattern pack (router_rules-style regex overlay) for known-simple intents. Deny-by-default (no match → no decision) and fail closed on invalid regex (the rule is rejected at load and never applies).
  3. safe_default — context-fit aware safe economical/frontier default (degraded_safe_default).

Explain/telemetry expose route_path (neural | learned | heuristic | safe_default) plus route_path_confidence on every decision; degraded decisions carry reason codes degraded_learned_route, degraded_pattern_<rule_id>, or degraded_safe_default. A learned/pattern suggestion toward a cheaper tier is only honored when the cheap tool-use cue estimate is below pattern_tool_use_ceiling — a cheap overlay never alone overrides a predicted capability shortfall.

| Knob (degraded_route operator config) | Default | Effect | |------|---------|--------| | enabled | true | When false, neural failures use the legacy safe_default stage pass-through | | learned_min_confidence | 0.6 | Minimum learned-entry confidence to honor a tier suggestion | | learned_max_entries | 512 | Learned-map cap per key space (FIFO eviction) | | pattern_tool_use_ceiling | 0.3 | Tool-use cue ceiling for honoring cheaper-tier learned/pattern suggestions | | fail_closed_on_missing_weights | false | When true, missing/placeholder neural weights fail closed: the matcher throws before paying embedding cost and the pipeline routes through the degraded sandwich (neural_misconfigured) with the SP-251 reason codes on the decision. When false (default), routing stays fail-open — placeholder weights score with neutral defaults and the reason codes are advisory telemetry only |

Missing-weights reason codes (v0.21.0, #148). When the HyDRA matcher cannot load real neural weights, the decision surfaces structured reason codes (src/domain/matching/missing-weights-reason-codes.ts) on the routing decision / requirement_reason_codes — visible in explain (pi router explain / POST /v1/route/explain), /smart-router history, and telemetry — not stderr-only:

| Reason code | Meaning | Operator action | |-------------|---------|-----------------| | hydra_weights_missing | HyDRA projection-head weights artifact is absent or unloadable, so neural scoring is running on fallback behavior | Restore the weights artifact (see HyDRA model cache); routing continues fail-open unless fail_closed_on_missing_weights is set | | k4_heads_placeholder | ModernBERT K4 heads are placeholder (untrained) weights — scores are neutral defaults, not learned predictions | Regenerate/install the trained K4 heads artifact; treat current K4 scores as non-authoritative |

With the default fail-open behavior these codes are advisory: the route proceeds with safe defaults. Set degraded_route.fail_closed_on_missing_weights: true when you prefer an explicit degraded-route decision (sandwich chain above, route_path records the branch taken) over silently routing on placeholder neural scores.

Distinct from soft heat affinity (healthy-path bias): this is failover / skip-expensive-stage only. Routing remains pre-generation — no FrugalGPT-style cascades (see routing-roadmap.md §1).

Virtual cost v2 (v0.5.0 subscription economics)

Virtual cost v2 extends SP-096 flat quota_cost_per_1m with deterministic subscription-window economics (#78). It inflates effective frontier cost late in a rolling quota window and credits warm prefix-cache value on active pins — without MDP or reinforcement-learning quota policy (SeqRoute HBR+CQL is deferred).

Formula (per turn)

effective_cost_usd = base × λ + quota_arbitrage_premium + exhaustion_risk_premium + kv_cache_savings

| Component | Meaning | |-----------|---------| | base | SP-096 subscription virtual cost (quota_cost_per_1m) or sticker fallback_cost_per_1m | | λ (quota decay) | Multiplier rising from 1 at full window toward lambda_max_multiplier as budget depletes | | Quota arbitrage premium | Opportunity-cost uplift for burning subscription quota late in the window | | Exhaustion risk premium | Extra penalty when remaining window fraction falls below exhaustion_risk_threshold | | KV-cache savings | Negative credit when pin is active and prefix is warm (prefix_cache_discount × prefix_cache_weight) |

Window position

Rolling-window position is supplied to the router pipeline as quotaWindowPosition (library API / telemetry integration). Use remaining_window_fraction in [0, 1] (1 = full budget). Optionally derive it from elapsed time and consumed quota via deriveRemainingWindowFraction(elapsed_seconds, consumed_fraction) in virtual-cost-v2.ts (defaults assume a Cursor-style 5h window).

Quota window feed (producer, #125)

There is no universal cross-provider "remaining quota" API, so src/domain/pricing/quota-window-feed.ts produces the position via an adapter + degrade chain: (1) a provider QuotaWindowAdapter when a trustworthy signal exists, (2) a telemetry-derived pool-level burn estimate over the rolling window (subscription-pool models = fleet entries with quota_cost_per_1m; enabled via SMART_ROUTER_QUOTA_POOL_BUDGET_TOKENS + SMART_ROUTER_QUOTA_WINDOW_SECONDS), (3) omit → flat virtual cost + SP-097 exhaustion failover. The smart-router extension resolves the feed at fleet rebuild and passes it through createDispatchOptions (SP-173 wiring gap closed). Soft bias only — no hard ban at any threshold; SP-097 reactive failover remains the safety net when the feed is missing or stale. Per-model fractions for shared pools are never invented.

When quotaWindowPosition is omitted, λ stays at 1 and quota premiums are zero — behavior matches SP-096 flat virtual cost.

Operator knobs (VirtualCostV2Config — wire through RouterPipeline options today; defaults in DEFAULT_VIRTUAL_COST_V2_CONFIG):

| Knob | Default | Effect | |------|---------|--------| | window_duration_seconds | 18000 (5h) | Rolling window length for time-based remaining fraction | | lambda_decay_exponent | 2 | Curvature of λ rise as window depletes | | lambda_max_multiplier | 3 | λ cap at exhaustion | | quota_arbitrage_weight | 0.5 | Weight on late-window arbitrage premium | | exhaustion_risk_weight | 1 | Weight on exhaustion risk below threshold | | exhaustion_risk_threshold | 0.2 | Remaining fraction below which exhaustion premium applies | | prefix_cache_discount | 0.9 | Assumed prefix-cache discount on warm tokens | | prefix_cache_weight | 0.2 | Retained future cache value (aligned with SAAR SMART_ROUTER_PREFIX_CACHE_WEIGHT) |

Where v2 applies

  • Expected-cost tier selection — frontier/composer effective cost rises near window exhaustion; economical tiers can win when subscription quota is scarce.
  • Cache breakeven gate — marginal switch savings and observability use v2 when quotaWindowPosition is set; KV credit on the pinned model reduces marginal savings and can block unnecessary pin breaks.

Dogfood verification

  1. Configure a fleet with subscription quota_cost_per_1m on cursor/composer frontier models (see config/models.yaml.example).
  2. Run routing with quotaWindowPosition: { remaining_window_fraction: 0.05 } via library RouterPipeline options — inspect tier_selection / expected-cost rationale for v2 λ=, quota_premium=, exhaustion=, cache_credit= strings.
  3. On a warm pinned session with low remaining_window_fraction, trigger a tool_result sub-route — when cache credit plus reprime math fails breakeven, expect pin hold (breakeven_blocked) in routing logs and features.breakeven on explain.
  4. Compare remaining_window_fraction: 1 vs 0.02 on the same request — late-window runs should show higher frontier effective_cost_usd in features.tier_selection.tier_costs[].virtual_cost_v2.

See routing-roadmap.md §2 P2 and GitHub #78.

Usage actuals (post-turn capture)

When pi reports an assistant message usage object after a delegated turn, the smart-router persists it onto that turn's routing telemetry row (#164): actual_cost_usd, actual_input_tokens, actual_output_tokens, and cache read/write token counts — while retaining estimated_cost_usd. Capture fails open: library embeds and non-pi hosts that report no usage simply leave the actual fields null, and a telemetry write error never fails the route. Subscription/OAuth models report cost.total === 0: token actuals are still recorded, but no USD is invented — /smart-router stats labels its totals cost_basis: 'actual' | 'estimated' | 'mixed' accordingly.

Rolling cost calibration (v0.20.0 usage actuals)

Rolling cost calibration soft-biases future cost estimates with the ratio your models actually bill versus what the router estimated (#164). It is built from the privacy-safe post-turn usage actuals — no new state to configure, and prompt/message bodies are never touched.

How it works

  1. Every routed turn records an estimated_cost_usd (input tokens × catalog rate) and, when pi reports it, an actual_cost_usd on the same telemetry row.
  2. buildCostCalibrationPrior derives per-model and per-tier mean actual / estimate ratios over the rolling telemetry window. Per-pair outliers are clamped (±10×) before averaging; the aggregate is clamped to a soft band of [0.5, 2.0] so calibration can never hard-ban or hard-favor a model on cost alone.
  3. When warm (≥ 3 usable pairs per bucket), the ratio multiplies the tier's base per-1M cost before the virtual-cost v2 λ/premium/KV chain — so quota decay and cache credits compound on the calibrated base. Model buckets win over tier buckets.
  4. estimateRoutingCost accepts the same prior as an optional argument, soft-biasing per-request cost estimates identically.

Cold start fails open. No prior, an empty prior, or a bucket below the warmup threshold resolves ratio 1 — the catalog estimate is used unchanged. Subscription rows (host reports cost.total === 0) never contribute: stats and calibration never invent USD.

Where it applies

  • Expected-cost tier selection — pass costCalibration into selectTierByExpectedCost / computeExpectedCost (library API, same pattern as heatBias). A warm ratio that doubles an economical tier's effective cost can flip the next selection to frontier; the price-delta and pin-economics hard gates still apply after the soft bias.
  • Pre-route estimatesestimateRoutingCost(model, request, catalog, calibration?); the router pipeline's uncalibrated call is unchanged.
  • /smart-router stats — the aggregate's JSON snapshot (aggregateSessionStats / buildStatsSnapshot, the automation/MCP surface) carries a cost_calibration array (model buckets first, then tier buckets, each {key, kind, ratio, samples}); omitted entirely when cold so automation can treat absence as catalog-only. The human-readable stats text keeps rendering cost basis and role breakdown only.

Observing the bias — run with SMART_ROUTER_LOG_ROUTING=1 and read the expected-cost gate line: calibrated winners carry a [cost-calib ×N.NN from rolling actuals (SP-242)] note on the rationale, and each tier's calibrationRatio appears in the expected-cost breakdown (features.tier_selection.tier_costs[]).

Knobs (DEFAULT_COST_CALIBRATION_CONFIG): minSamples 3 (warmup), minRatio/maxRatio 0.5/2.0 (soft band), sampleMinRatio/sampleMaxRatio 0.1/10 (per-pair outlier clamp). Not a vendor peak-clock schedule — see #165 for time-of-day pricing.

Peak/off-peak pricing adapters (v0.20.0, #165)

Two vendors publish documented time-of-day rate cards. The peak-pricing adapters (src/domain/pricing/peak-pricing.js, SP-243) soft-bias the resolved cost-per-1M by the current pricing window — they never hard-ban a model, and non-target providers (OpenAI / Anthropic / Gemini / local / unknown) always resolve window: 'none' with multiplier 1 (fail open).

| Vendor | Adapter | Peak window | Off-peak rate | Docs | |--------|---------|-------------|---------------|------| | Z.ai GLM Coding Plan | zai (matches zai/glm/zhipu providers and glm-* ids) | Mon–Fri 14:00–18:00 Asia/Singapore (UTC+8) | 0.5× the standard credit rate | Z.ai GLM Coding Plan docs | | DeepSeek pay-as-you-go API | deepseek (matches deepseek provider / ids) | Mon–Fri 01:00–04:00 and 06:00–10:00 UTC | ½ the peak rate on cache-hit input, cache-miss input, and output | DeepSeek Models & Pricing |

Z.ai plan profiles. The default plan profile is credits — off-peak usage at 0.5× the standard credit rate, peak at 1×. Legacy plans (e.g. GLM-5.3 legacy at 3× peak / 1× off-peak, Flash at 1.2× / 0.4×) share the same window but use different multipliers; they are available only via an explicit operator override (PeakPricingConfig.zai: plan_profile: 'legacy' plus documented peak_multiplier / off_peak_multiplier). The adapter never scrapes the live account plan — legacy multipliers are documented override inputs, not detected state.

Configuration. Adapters are on by default; PeakPricingConfig.enabled: false returns to flat rates. PeakPricingConfig.deepseek.off_peak_multiplier overrides the documented 0.5 off-peak discount if DeepSeek changes its schedule.

Observing the window. Every routed turn records pricing_window: 'peak' | 'off_peak' | 'none' on its telemetry row (SP-243). With SMART_ROUTER_LOG_ROUTING=1, the routing-decision payload surfaces the rationale as top-level pricing_window plus peak_pricing_summary (window, cost_multiplier, adapter_id) on the canonical buildRoutingDecisionLogPayload, and pricing_window + peak_pricing on the extension stderr logger (SP-244).

P(success) training export (baseline classifier)

When SMART_ROUTER_DATASET=1, the router records privacy-safe dataset rows and behavioral outcome labels. Export labeled training data from pi:

/smart-router export dataset [--limit N]

Each JSONL row joins dataset features with success_label and outcome_signals. Success means no negative outcome signals were recorded for that request_id (for example model_override or feedback_bad mark failure). Prompt plaintext is never included.

Behavioral-first bootstrap (zero manual labels)

Primary path for [#110](https://github.com/beettlle/pi-smar