pi-smart-router 0.1.0
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/.pi/extensions/smart-router/command-formatters.ts +167 -0
- package/.pi/extensions/smart-router/commands.ts +242 -0
- package/.pi/extensions/smart-router/dataset-export.ts +142 -0
- package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
- package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
- package/.pi/extensions/smart-router/extension-setup.ts +115 -0
- package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
- package/.pi/extensions/smart-router/index.ts +115 -0
- package/.pi/extensions/smart-router/package.json +3 -0
- package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
- package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
- package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
- package/.pi/extensions/smart-router/routing-context.ts +171 -0
- package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
- package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
- package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
- package/.pi/extensions/smart-router/types.ts +90 -0
- package/.pi/extensions/smart-router/utils.ts +58 -0
- package/README.md +593 -0
- package/bin/pi-smart-router.mjs +79 -0
- package/config/.gitkeep +0 -0
- package/config/hydra-projection-weights.json.example +1169 -0
- package/config/models.yaml.example +58 -0
- package/config/p-success-weights.json.example +19 -0
- package/config/routing-calibration.json.example +1605 -0
- package/config/routing-centroids.json.example +1570 -0
- package/config/routing-clusters.yaml.example +41 -0
- package/data/contrib/example.json +25 -0
- package/dist/api/explain/router-explain.d.ts +47 -0
- package/dist/api/explain/router-explain.d.ts.map +1 -0
- package/dist/api/explain/router-explain.js +101 -0
- package/dist/api/explain/router-explain.js.map +1 -0
- package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
- package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
- package/dist/api/middleware/pi-router-middleware.js +83 -0
- package/dist/api/middleware/pi-router-middleware.js.map +1 -0
- package/dist/cli/smart-router-cli.d.ts +85 -0
- package/dist/cli/smart-router-cli.d.ts.map +1 -0
- package/dist/cli/smart-router-cli.js +293 -0
- package/dist/cli/smart-router-cli.js.map +1 -0
- package/dist/config/defaults.d.ts +7 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +33 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/models-loader.d.ts +22 -0
- package/dist/config/models-loader.d.ts.map +1 -0
- package/dist/config/models-loader.js +51 -0
- package/dist/config/models-loader.js.map +1 -0
- package/dist/config/pi-model-mapper.d.ts +40 -0
- package/dist/config/pi-model-mapper.d.ts.map +1 -0
- package/dist/config/pi-model-mapper.js +216 -0
- package/dist/config/pi-model-mapper.js.map +1 -0
- package/dist/config/routing-clusters-loader.d.ts +35 -0
- package/dist/config/routing-clusters-loader.d.ts.map +1 -0
- package/dist/config/routing-clusters-loader.js +100 -0
- package/dist/config/routing-clusters-loader.js.map +1 -0
- package/dist/domain/delegation/delegation-context.d.ts +23 -0
- package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
- package/dist/domain/delegation/delegation-context.js +73 -0
- package/dist/domain/delegation/delegation-context.js.map +1 -0
- package/dist/domain/delegation/execution-ledger.d.ts +19 -0
- package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
- package/dist/domain/delegation/execution-ledger.js +23 -0
- package/dist/domain/delegation/execution-ledger.js.map +1 -0
- package/dist/domain/delegation/output-headroom.d.ts +39 -0
- package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
- package/dist/domain/delegation/output-headroom.js +78 -0
- package/dist/domain/delegation/output-headroom.js.map +1 -0
- package/dist/domain/matching/cluster-matcher.d.ts +69 -0
- package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
- package/dist/domain/matching/cluster-matcher.js +294 -0
- package/dist/domain/matching/cluster-matcher.js.map +1 -0
- package/dist/domain/matching/embedding-provider.d.ts +21 -0
- package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
- package/dist/domain/matching/embedding-provider.js +42 -0
- package/dist/domain/matching/embedding-provider.js.map +1 -0
- package/dist/domain/matching/hydra-input.d.ts +18 -0
- package/dist/domain/matching/hydra-input.d.ts.map +1 -0
- package/dist/domain/matching/hydra-input.js +40 -0
- package/dist/domain/matching/hydra-input.js.map +1 -0
- package/dist/domain/matching/hydra-matcher.d.ts +103 -0
- package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
- package/dist/domain/matching/hydra-matcher.js +275 -0
- package/dist/domain/matching/hydra-matcher.js.map +1 -0
- package/dist/domain/pinning/cache-economics.d.ts +53 -0
- package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
- package/dist/domain/pinning/cache-economics.js +72 -0
- package/dist/domain/pinning/cache-economics.js.map +1 -0
- package/dist/domain/pinning/loop-escalation.d.ts +41 -0
- package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
- package/dist/domain/pinning/loop-escalation.js +137 -0
- package/dist/domain/pinning/loop-escalation.js.map +1 -0
- package/dist/domain/pinning/session-pinner.d.ts +81 -0
- package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
- package/dist/domain/pinning/session-pinner.js +237 -0
- package/dist/domain/pinning/session-pinner.js.map +1 -0
- package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
- package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
- package/dist/domain/pinning/sub-route-policy.js +72 -0
- package/dist/domain/pinning/sub-route-policy.js.map +1 -0
- package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
- package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
- package/dist/domain/pipeline/router-pipeline.js +807 -0
- package/dist/domain/pipeline/router-pipeline.js.map +1 -0
- package/dist/domain/pipeline/safe-default.d.ts +25 -0
- package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
- package/dist/domain/pipeline/safe-default.js +41 -0
- package/dist/domain/pipeline/safe-default.js.map +1 -0
- package/dist/domain/refactor-guardrails.d.ts +48 -0
- package/dist/domain/refactor-guardrails.d.ts.map +1 -0
- package/dist/domain/refactor-guardrails.js +129 -0
- package/dist/domain/refactor-guardrails.js.map +1 -0
- package/dist/domain/routing/context-fit.d.ts +53 -0
- package/dist/domain/routing/context-fit.d.ts.map +1 -0
- package/dist/domain/routing/context-fit.js +142 -0
- package/dist/domain/routing/context-fit.js.map +1 -0
- package/dist/domain/routing/expected-cost.d.ts +63 -0
- package/dist/domain/routing/expected-cost.d.ts.map +1 -0
- package/dist/domain/routing/expected-cost.js +177 -0
- package/dist/domain/routing/expected-cost.js.map +1 -0
- package/dist/domain/routing/p-success-classifier.d.ts +130 -0
- package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
- package/dist/domain/routing/p-success-classifier.js +350 -0
- package/dist/domain/routing/p-success-classifier.js.map +1 -0
- package/dist/domain/routing/tier-features.d.ts +70 -0
- package/dist/domain/routing/tier-features.d.ts.map +1 -0
- package/dist/domain/routing/tier-features.js +174 -0
- package/dist/domain/routing/tier-features.js.map +1 -0
- package/dist/domain/routing/tool-history-guard.d.ts +32 -0
- package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
- package/dist/domain/routing/tool-history-guard.js +110 -0
- package/dist/domain/routing/tool-history-guard.js.map +1 -0
- package/dist/domain/scoring/multi-objective.d.ts +48 -0
- package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
- package/dist/domain/scoring/multi-objective.js +116 -0
- package/dist/domain/scoring/multi-objective.js.map +1 -0
- package/dist/domain/triage/triage-engine.d.ts +43 -0
- package/dist/domain/triage/triage-engine.d.ts.map +1 -0
- package/dist/domain/triage/triage-engine.js +317 -0
- package/dist/domain/triage/triage-engine.js.map +1 -0
- package/dist/domain/triage/turn-envelope.d.ts +17 -0
- package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
- package/dist/domain/triage/turn-envelope.js +81 -0
- package/dist/domain/triage/turn-envelope.js.map +1 -0
- package/dist/domain/types/entities.d.ts +303 -0
- package/dist/domain/types/entities.d.ts.map +1 -0
- package/dist/domain/types/entities.js +6 -0
- package/dist/domain/types/entities.js.map +1 -0
- package/dist/domain/types/index.d.ts +4 -0
- package/dist/domain/types/index.d.ts.map +1 -0
- package/dist/domain/types/index.js +2 -0
- package/dist/domain/types/index.js.map +1 -0
- package/dist/domain/types/schemas.d.ts +346 -0
- package/dist/domain/types/schemas.d.ts.map +1 -0
- package/dist/domain/types/schemas.js +241 -0
- package/dist/domain/types/schemas.js.map +1 -0
- package/dist/domain/types/store-port.d.ts +44 -0
- package/dist/domain/types/store-port.d.ts.map +1 -0
- package/dist/domain/types/store-port.js +6 -0
- package/dist/domain/types/store-port.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/infra/gemini-provider.d.ts +28 -0
- package/dist/infra/gemini-provider.d.ts.map +1 -0
- package/dist/infra/gemini-provider.js +112 -0
- package/dist/infra/gemini-provider.js.map +1 -0
- package/dist/infra/telemetry.d.ts +51 -0
- package/dist/infra/telemetry.d.ts.map +1 -0
- package/dist/infra/telemetry.js +100 -0
- package/dist/infra/telemetry.js.map +1 -0
- package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
- package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
- package/dist/infrastructure/delegation/provider-error.js +179 -0
- package/dist/infrastructure/delegation/provider-error.js.map +1 -0
- package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
- package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
- package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
- package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
- package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
- package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
- package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
- package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
- package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
- package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
- package/dist/infrastructure/hardware/hardware-probe.js +199 -0
- package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
- package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
- package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
- package/dist/infrastructure/local/local-zero-tier.js +92 -0
- package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
- package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
- package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
- package/dist/infrastructure/persistence/memory-store.js +87 -0
- package/dist/infrastructure/persistence/memory-store.js.map +1 -0
- package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
- package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
- package/dist/infrastructure/persistence/sqlite-store.js +658 -0
- package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
- package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
- package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
- package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
- package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
- package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
- package/dist/infrastructure/pricing/price-broker.js +133 -0
- package/dist/infrastructure/pricing/price-broker.js.map +1 -0
- package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
- package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
- package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
- package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
- package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
- package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
- package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
- package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
- package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
- package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
- package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
- package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
- package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
- package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
- package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
- package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
- package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
- package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
- package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
- package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
- package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
- package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
- package/package.json +110 -0
- package/skills/router-backlog-orchestrator/SKILL.md +183 -0
- package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
- package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
- package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
- package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
- package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
- package/src/api/explain/.gitkeep +0 -0
- package/src/api/explain/router-explain.ts +146 -0
- package/src/api/middleware/.gitkeep +0 -0
- package/src/api/middleware/pi-router-middleware.ts +169 -0
- package/src/cli/smart-router-cli.ts +436 -0
- package/src/config/.gitkeep +0 -0
- package/src/config/defaults.ts +35 -0
- package/src/config/models-loader.ts +68 -0
- package/src/config/pi-model-mapper.ts +285 -0
- package/src/config/routing-clusters-loader.ts +148 -0
- package/src/domain/delegation/delegation-context.ts +111 -0
- package/src/domain/delegation/execution-ledger.ts +34 -0
- package/src/domain/delegation/output-headroom.ts +131 -0
- package/src/domain/matching/.gitkeep +0 -0
- package/src/domain/matching/cluster-matcher.ts +474 -0
- package/src/domain/matching/embedding-provider.ts +79 -0
- package/src/domain/matching/hydra-input.ts +53 -0
- package/src/domain/matching/hydra-matcher.ts +454 -0
- package/src/domain/pinning/.gitkeep +0 -0
- package/src/domain/pinning/cache-economics.ts +128 -0
- package/src/domain/pinning/loop-escalation.ts +181 -0
- package/src/domain/pinning/session-pinner.ts +388 -0
- package/src/domain/pinning/sub-route-policy.ts +119 -0
- package/src/domain/pipeline/.gitkeep +0 -0
- package/src/domain/pipeline/router-pipeline.ts +1139 -0
- package/src/domain/pipeline/safe-default.ts +70 -0
- package/src/domain/refactor-guardrails.ts +202 -0
- package/src/domain/routing/context-fit.ts +221 -0
- package/src/domain/routing/expected-cost.ts +320 -0
- package/src/domain/routing/p-success-classifier.ts +511 -0
- package/src/domain/routing/tier-features.ts +290 -0
- package/src/domain/routing/tool-history-guard.ts +159 -0
- package/src/domain/scoring/.gitkeep +0 -0
- package/src/domain/scoring/multi-objective.ts +175 -0
- package/src/domain/triage/.gitkeep +0 -0
- package/src/domain/triage/triage-engine.ts +373 -0
- package/src/domain/triage/turn-envelope.ts +104 -0
- package/src/domain/types/.gitkeep +0 -0
- package/src/domain/types/entities.ts +384 -0
- package/src/domain/types/index.ts +41 -0
- package/src/domain/types/schemas.ts +289 -0
- package/src/domain/types/store-port.ts +59 -0
- package/src/index.ts +103 -0
- package/src/infra/gemini-provider.ts +142 -0
- package/src/infra/telemetry.ts +169 -0
- package/src/infrastructure/delegation/provider-error.ts +251 -0
- package/src/infrastructure/gateway/.gitkeep +0 -0
- package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
- package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
- package/src/infrastructure/hardware/.gitkeep +0 -0
- package/src/infrastructure/hardware/hardware-probe.ts +276 -0
- package/src/infrastructure/local/.gitkeep +0 -0
- package/src/infrastructure/local/local-zero-tier.ts +151 -0
- package/src/infrastructure/persistence/.gitkeep +0 -0
- package/src/infrastructure/persistence/memory-store.ts +122 -0
- package/src/infrastructure/persistence/sqlite-store.ts +900 -0
- package/src/infrastructure/pricing/.gitkeep +0 -0
- package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
- package/src/infrastructure/pricing/price-broker.ts +183 -0
- package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
- package/src/infrastructure/telemetry/.gitkeep +0 -0
- package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
- package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
- package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
- package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
- package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
- package/src/infrastructure/telemetry/telemetry-limits.ts +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# pi-smart-router
|
|
2
|
+
|
|
3
|
+
**Auto-model router middleware for the [pi](https://pi.dev) coding agent.**
|
|
4
|
+
|
|
5
|
+
> **v0.1.0** is initial development (SemVer `0.y.z`). The public API and routing behavior may change until `1.0.0`.
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
| pi-smart-router is | pi-smart-router is not |
|
|
10
|
+
|--------------------|------------------------|
|
|
11
|
+
| A pi extension that auto-selects the best model per request | A replacement for pi or your LLM provider |
|
|
12
|
+
| A three-tier router: local, economical cloud, frontier cloud | A post-generation output judger (FrugalGPT-style) |
|
|
13
|
+
| Cache-aware with session pinning to preserve prompt-cache economics | A turn-by-turn model switcher that shatters provider caching |
|
|
14
|
+
| Registry-driven in pi (no YAML copy for normal use) | An RL-trained router requiring agent trace datasets |
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
request → hardware probe → loop escalation → turn envelope → context-fit gate
|
|
20
|
+
→ low-intensity tier gate → session pin → deterministic triage
|
|
21
|
+
→ local zero-tier → HyDRA embedding matcher → safe cloud default
|
|
22
|
+
→ context overflow fallback
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
| Stage | Budget | What it does |
|
|
28
|
+
|-------|--------|--------------|
|
|
29
|
+
| Hardware Probe | — | Checks platform/RAM/battery to gate local inference |
|
|
30
|
+
| Loop Escalation | — | Detects repeated identical tool failures; escalates session to frontier |
|
|
31
|
+
| Turn Envelope | <2ms | Classifies turn type: tool_result, planning, subagent, main_loop |
|
|
32
|
+
| Context-Fit Gate | — | Filters fleet to models whose context window fits estimated input tokens |
|
|
33
|
+
| Low-Intensity Gate | — | Structural tier hint, cluster match, and P(success) expected-cost scoring |
|
|
34
|
+
| Session Pin | <1ms | Returns pinned model if session has one; breaks pin on compaction or overflow |
|
|
35
|
+
| Deterministic Triage | <5ms | Aho-Corasick keyword scan + cyclomatic complexity analysis |
|
|
36
|
+
| Local Zero-Tier | <15ms | Pings LM Studio + Ollama in parallel; routes locally when eligible |
|
|
37
|
+
| HyDRA Matcher | 80-120ms | ONNX embeddings, 3D requirement projection, shortfall gate, multi-objective scoring |
|
|
38
|
+
| Safe Cloud Default | — | First healthy economical-cloud model (context-fit aware) |
|
|
39
|
+
| Context Overflow Fallback | — | Escalates to largest-fit model when economical tiers cannot fit |
|
|
40
|
+
|
|
41
|
+
## Research lineage
|
|
42
|
+
|
|
43
|
+
pi-smart-router builds on ideas from several production and research routing systems:
|
|
44
|
+
|
|
45
|
+
- **Adopted:** [GitHub Copilot HyDRA](https://arxiv.org/abs/2409.08379) (shortfall matching decoupled from model identities), Zero-Tier local edge-cache pattern, [Weave Router](https://github.com/workweave/router) session pinning and multi-objective selection
|
|
46
|
+
- **Rejected:** FrugalGPT sequential cascading (tail latency), RouteLLM matrix factorization (confounder vulnerability), turn-by-turn dynamic routing (cache destruction)
|
|
47
|
+
|
|
48
|
+
See [docs/PRD.md](docs/PRD.md) for full architectural justification.
|
|
49
|
+
|
|
50
|
+
## Prerequisites
|
|
51
|
+
|
|
52
|
+
| Dependency | Required | Notes |
|
|
53
|
+
|------------|----------|-------|
|
|
54
|
+
| [Node.js](https://nodejs.org/) >= 22 | Yes | ES module package; matches CI and `package.json` engines |
|
|
55
|
+
| [pi](https://pi.dev) coding agent | Yes | Extension host |
|
|
56
|
+
| macOS Apple Silicon | MVP | Primary supported platform |
|
|
57
|
+
| Linux (x64/arm64) | Experimental | Probe logic supported; not validated on real hardware |
|
|
58
|
+
| Windows (x64/arm64) | Experimental | Probe logic supported; not validated on real hardware |
|
|
59
|
+
| [LM Studio](https://lmstudio.ai/) or [Ollama](https://ollama.com) | Optional | Required for zero-tier local routing |
|
|
60
|
+
| Authenticated cloud providers in pi | Recommended | Anthropic, OpenAI, Google, etc. |
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
> **Security:** Pi packages run with full system access. Extensions execute arbitrary code. Review source before installing third-party packages ([pi packages docs](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md)).
|
|
65
|
+
|
|
66
|
+
### Via pi (recommended)
|
|
67
|
+
|
|
68
|
+
Install from [npm](https://www.npmjs.com/package/pi-smart-router) / [pi.dev/packages](https://pi.dev/packages):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pi install npm:pi-smart-router
|
|
72
|
+
pi --list-models | grep smart-router
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Project-local install (writes to `.pi/settings.json`):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pi install -l npm:pi-smart-router
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then in pi:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
/model smart-router/auto
|
|
85
|
+
/smart-router status
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**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.
|
|
89
|
+
|
|
90
|
+
### Via npm (library API)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install pi-smart-router
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use `createRouter()` / `createRouterFromFleet()` for programmatic integration without the pi extension. See [Optional: YAML fleet (library API)](#optional-yaml-fleet-library-api).
|
|
97
|
+
|
|
98
|
+
### From source (contributors)
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
git clone https://github.com/beettlle/pi-smart-router.git
|
|
102
|
+
cd pi-smart-router
|
|
103
|
+
npm install
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
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](#develop-from-clone)).
|
|
107
|
+
|
|
108
|
+
## Quick start
|
|
109
|
+
|
|
110
|
+
After installing via `pi install npm:pi-smart-router` (or from clone — see below):
|
|
111
|
+
|
|
112
|
+
1. Authenticate providers (`/login`) and enable models in your scoped list if you use one (`/scoped-models`)
|
|
113
|
+
2. `/model smart-router/auto` — every turn runs through the routing pipeline
|
|
114
|
+
3. `/smart-router status` or `/smart-router history` — inspect routing decisions
|
|
115
|
+
|
|
116
|
+
Set `SMART_ROUTER_LOG_ROUTING=1` before starting pi to print each routing decision to stderr (see [Environment variables](#environment-variables)).
|
|
117
|
+
|
|
118
|
+
## Use with pi
|
|
119
|
+
|
|
120
|
+
Detailed steps for the operator path above.
|
|
121
|
+
|
|
122
|
+
### Installed via npm
|
|
123
|
+
|
|
124
|
+
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.
|
|
125
|
+
|
|
126
|
+
After auth or model list changes, restart pi or run `/reload`.
|
|
127
|
+
|
|
128
|
+
### Develop from clone
|
|
129
|
+
|
|
130
|
+
Project-local extensions under `.pi/extensions/` load only after the project is trusted. Without trust, the smart-router provider is never registered — `smart-router` will not appear in `/scoped-models` or `/model`, and `/smart-router` commands will not exist.
|
|
131
|
+
|
|
132
|
+
**On first run**, pi prompts you to trust the project when it detects `.pi/extensions/`. Accept the prompt.
|
|
133
|
+
|
|
134
|
+
**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.
|
|
135
|
+
|
|
136
|
+
**Verify the extension loaded** (from the repo root):
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
cd pi-smart-router
|
|
140
|
+
pi --list-models | grep smart-router
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
You should see `smart-router auto`. If the line is missing:
|
|
144
|
+
|
|
145
|
+
1. Confirm `pi` was started with cwd at this repo root (not a parent directory).
|
|
146
|
+
2. Confirm the project is trusted (`/trust`, or check `~/.pi/agent/trust.json`).
|
|
147
|
+
3. Restart pi or run `/reload` after trusting.
|
|
148
|
+
|
|
149
|
+
Non-interactive one-shot checks can pass `--approve` to trust project-local resources for that run only.
|
|
150
|
+
|
|
151
|
+
### Select the auto model
|
|
152
|
+
|
|
153
|
+
Switch to the smart-router provider (from any directory after npm install, or from repo root when developing from clone):
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
/model smart-router/auto
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
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.
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
#### `cursor/auto` vs `smart-router/auto`
|
|
164
|
+
|
|
165
|
+
pi exposes two different **auto** models. They are easy to confuse but play different roles:
|
|
166
|
+
|
|
167
|
+
| Model | Provider | Role |
|
|
168
|
+
|-------|----------|------|
|
|
169
|
+
| `smart-router/auto` | `smart-router` (this extension) | Runs the routing pipeline on every turn and **delegates** to whichever underlying model HyDRA selects |
|
|
170
|
+
| `cursor/auto` | `cursor` (pi registry) | Cursor's opaque auto model — **direct** inference target when selected; Cursor picks the backend model |
|
|
171
|
+
|
|
172
|
+
**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](#gemini-thought_signature-400-errors) excludes Google models.
|
|
173
|
+
|
|
174
|
+
**When to pin `/model cursor/auto` directly (bypass the router):**
|
|
175
|
+
|
|
176
|
+
- You want Cursor's opaque auto selection on every turn with no routing overhead
|
|
177
|
+
- You are debugging Cursor SDK auth or delegation outside the router
|
|
178
|
+
- You need a stable, non-routed session for comparison with routed behavior
|
|
179
|
+
|
|
180
|
+
**When to use `smart-router/auto`:**
|
|
181
|
+
|
|
182
|
+
- You want cost/capability-aware model selection across your full authenticated fleet
|
|
183
|
+
- You rely on session pinning, failover, or `/smart-router status` / `history` telemetry
|
|
184
|
+
- Tool-heavy sessions should fall back to `cursor/auto` or other non-Gemini models automatically (see [pi-smart-router#38](https://github.com/beettlle/pi-smart-router/issues/38))
|
|
185
|
+
|
|
186
|
+
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](https://github.com/beettlle/pi-smart-router/issues/40), [pi-smart-router#70](https://github.com/beettlle/pi-smart-router/issues/70)). Related: [pi-smart-router#23](https://github.com/beettlle/pi-smart-router/issues/23) (turn envelope / pin order), [pi-smart-router#37](https://github.com/beettlle/pi-smart-router/issues/37) (Gemini `thought_signature` errors).
|
|
187
|
+
|
|
188
|
+
#### Cursor subscription quota vs API cost
|
|
189
|
+
|
|
190
|
+
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](https://github.com/beettlle/pi-smart-router/issues/70)). Economical API models (e.g. `gemini-flash-lite`) can outscore `composer-latest` on routine `main_loop` turns when capabilities are sufficient.
|
|
191
|
+
|
|
192
|
+
**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.
|
|
193
|
+
|
|
194
|
+
### Operator commands
|
|
195
|
+
|
|
196
|
+
| Command | Purpose |
|
|
197
|
+
|---------|---------|
|
|
198
|
+
| `/smart-router` | Same as `status` (default when no subcommand is given) |
|
|
199
|
+
| `/smart-router status` | Show fleet mode, fleet size, pricing freshness/staleness, and the last routing decision (stage, tier, selected model, latency) |
|
|
200
|
+
| `/smart-router history` | Show recent routing telemetry from SQLite (default limit; optional numeric limit, e.g. `/smart-router history 20`) |
|
|
201
|
+
| `/smart-router mode scoped` | Route only among pi's **enabled model patterns** (default) |
|
|
202
|
+
| `/smart-router mode all` | Route among **all authenticated models** in the registry |
|
|
203
|
+
| `/smart-router pricing refresh` | Manually fetch LiteLLM pricing from `LITELLM_PRICING_URL`, persist to SQLite, and rebuild the fleet with updated rates |
|
|
204
|
+
| `/smart-router export dataset [--limit N]` | Export opt-in routing dataset as JSONL (requires `SMART_ROUTER_DATASET=1`) |
|
|
205
|
+
| `/smart-router export telemetry-contrib [--limit N]` | Export privacy-safe community telemetry JSON for calibration contributions |
|
|
206
|
+
| `/smart-router feedback good\|bad` | Label the last auto-routed request outcome (requires `SMART_ROUTER_DATASET=1`) |
|
|
207
|
+
| `/smart-router unpin` | Clear the current session pin (in-memory and SQLite) so the next request runs the full routing pipeline |
|
|
208
|
+
|
|
209
|
+
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.
|
|
210
|
+
|
|
211
|
+
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`).
|
|
212
|
+
|
|
213
|
+
### 5. Verify
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run verify:ci
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Fleet behavior
|
|
220
|
+
|
|
221
|
+
When you use `smart-router/auto`, the extension does **not** read `config/models.yaml`. Instead:
|
|
222
|
+
|
|
223
|
+
1. **Discover** — `modelRegistry.getAvailable()` returns authenticated models from pi.
|
|
224
|
+
2. **Scope** — In `scoped` mode, filter to patterns from pi settings (`getEnabledModels()`). In `all` mode, use the full registry.
|
|
225
|
+
3. **Map** — `src/config/pi-model-mapper.ts` maps each pi model to a `ModelProfile` (tier, capabilities, pricing) using provider and model-id patterns.
|
|
226
|
+
4. **Route** — `createRouterFromFleet()` runs the 12-stage pipeline on each request.
|
|
227
|
+
5. **Delegate** — The extension resolves the chosen model in the registry and forwards the stream via pi-ai's built-in provider APIs.
|
|
228
|
+
|
|
229
|
+
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).
|
|
230
|
+
|
|
231
|
+
To refresh after auth or settings changes, restart pi or `/reload` extensions.
|
|
232
|
+
|
|
233
|
+
## Optional: YAML fleet (library API)
|
|
234
|
+
|
|
235
|
+
For programmatic integration **without** the pi extension, load a static fleet catalog from YAML and route via `GatewayDispatch.dispatch()`:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
cp config/models.yaml.example ./config/models.yaml
|
|
239
|
+
# Edit config/models.yaml — at least one model per tier
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { createRouter } from 'pi-smart-router';
|
|
244
|
+
|
|
245
|
+
const router = createRouter({ modelsPath: './config/models.yaml' });
|
|
246
|
+
router.register(piExtensionHooks); // lifecycle only: compaction + model override
|
|
247
|
+
|
|
248
|
+
const decision = await router.dispatch.dispatch(routingRequest);
|
|
249
|
+
// Embedder forwards inference to decision.selected_model_id
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Embedder integration paths
|
|
253
|
+
|
|
254
|
+
| Path | When to use | Routing | Lifecycle hooks |
|
|
255
|
+
|------|-------------|---------|-----------------|
|
|
256
|
+
| **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 |
|
|
257
|
+
| **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 |
|
|
258
|
+
|
|
259
|
+
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.
|
|
260
|
+
|
|
261
|
+
`createRouter()` returns a `RouterHandle`:
|
|
262
|
+
|
|
263
|
+
| Property | Type | Purpose |
|
|
264
|
+
|----------|------|---------|
|
|
265
|
+
| `middleware` | `PiRouterMiddleware` | Lifecycle hook registrar (`register`, `lifecycleHookState`) |
|
|
266
|
+
| `dispatch` | `GatewayDispatch` | Gateway with circuit breaker, failover, rate limiting |
|
|
267
|
+
| `fleet` | `readonly ModelProfile[]` | Loaded fleet catalog |
|
|
268
|
+
| `register` | `(hooks) => void` | Alias for `middleware.register` — attach pi lifecycle hooks |
|
|
269
|
+
|
|
270
|
+
You can also pass a pre-built fleet:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import { createRouterFromFleet } from 'pi-smart-router';
|
|
274
|
+
|
|
275
|
+
const router = createRouterFromFleet(myFleetProfiles);
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Example fleet entry:
|
|
279
|
+
|
|
280
|
+
```yaml
|
|
281
|
+
models:
|
|
282
|
+
- id: local-gemma-4-7b
|
|
283
|
+
tier: zero-tier
|
|
284
|
+
provider: lmstudio
|
|
285
|
+
endpoint: http://localhost:1234/v1
|
|
286
|
+
capabilities:
|
|
287
|
+
reasoning: 0.3
|
|
288
|
+
code_gen: 0.6
|
|
289
|
+
tool_use: 0.1
|
|
290
|
+
pricing:
|
|
291
|
+
registry_key: local/free
|
|
292
|
+
fallback_cost_per_1m: 0.0
|
|
293
|
+
|
|
294
|
+
- id: claude-3.5-haiku
|
|
295
|
+
tier: economical-cloud
|
|
296
|
+
provider: anthropic
|
|
297
|
+
# ...
|
|
298
|
+
|
|
299
|
+
- id: claude-3.5-sonnet
|
|
300
|
+
tier: frontier-cloud
|
|
301
|
+
provider: anthropic
|
|
302
|
+
# ...
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Tiers: `zero-tier`, `economical-cloud`, `frontier-cloud`. See [config/models.yaml.example](config/models.yaml.example).
|
|
306
|
+
|
|
307
|
+
### Routing cluster catalog (library API)
|
|
308
|
+
|
|
309
|
+
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.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
cp config/routing-clusters.yaml.example ./config/routing-clusters.yaml
|
|
313
|
+
cp config/routing-centroids.json.example ./config/routing-centroids.json
|
|
314
|
+
# Edit reference_prompts, min_similarity, and min_margin per cluster
|
|
315
|
+
# Regenerate centroids after catalog changes:
|
|
316
|
+
npm run routing:bootstrap-centroids
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
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.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { loadRoutingClusters } from 'pi-smart-router';
|
|
323
|
+
|
|
324
|
+
const catalog = await loadRoutingClusters({
|
|
325
|
+
filePath: './config/routing-clusters.yaml',
|
|
326
|
+
embedder: myTextEmbedder, // shared ONNX embedder (SP-100)
|
|
327
|
+
});
|
|
328
|
+
// Reason codes: cluster_${id} — e.g. cluster_low_stakes_general
|
|
329
|
+
|
|
330
|
+
// createClusterMatcher (cluster-matcher module) prefers routing-centroids.json when present.
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Cluster IDs are stable reason-code prefixes (`cluster_low_stakes_general`, `cluster_architecture`, etc.). See [config/routing-clusters.yaml.example](config/routing-clusters.yaml.example).
|
|
334
|
+
|
|
335
|
+
## Configuration
|
|
336
|
+
|
|
337
|
+
### Environment variables
|
|
338
|
+
|
|
339
|
+
| Variable | Default | Purpose |
|
|
340
|
+
|----------|---------|---------|
|
|
341
|
+
| `ROUTER_STATE_DB_PATH` | `./.pi-smart-router/state.db` | Override SQLite state store location (telemetry, pricing catalog, session data) |
|
|
342
|
+
| `SMART_ROUTER_LOG_ROUTING` | (unset) | Set to `1` to log each routing decision to stderr as JSON (debugging dogfood sessions) |
|
|
343
|
+
| `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](https://github.com/beettlle/pi-smart-router/issues/8). |
|
|
344
|
+
| `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](https://github.com/beettlle/pi-smart-router/issues/10). |
|
|
345
|
+
| `MODELS_YAML_PATH` | `./config/models.yaml` | Fleet catalog path (library API only) |
|
|
346
|
+
| `ROUTER_SAFE_DEFAULT_TIER` | `economical-cloud` | Fallback tier on any routing failure |
|
|
347
|
+
| `LITELLM_PRICING_URL` | — | LiteLLM pricing JSON source |
|
|
348
|
+
|
|
349
|
+
### P(success) training export (baseline classifier)
|
|
350
|
+
|
|
351
|
+
When `SMART_ROUTER_DATASET=1`, the router records privacy-safe dataset rows and behavioral outcome labels (model override, compaction pin break, `/smart-router feedback good|bad`). Export labeled training data from pi:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
/smart-router export dataset [--limit N]
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
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.
|
|
358
|
+
|
|
359
|
+
Train a baseline logistic scorer offline from the export (see `src/domain/routing/p-success-classifier.ts`):
|
|
360
|
+
|
|
361
|
+
- `trainFromExportJsonl(exportContent)` — fit coefficients from labeled JSONL
|
|
362
|
+
- `predictPSuccessCheap(features, weights)` — returns `P_success_cheap` in `[0, 1]`
|
|
363
|
+
|
|
364
|
+
Copy `config/p-success-weights.json.example` to `config/p-success-weights.json` and replace coefficients after training. **Minimum sample guidance:** collect at least **30** labeled economical-tier rows before relying on non-neutral predictions; below that threshold the classifier returns neutral `P_success_cheap = 0.5`. **Online inference** is active in the low-intensity gate; without trained weights the router uses neutral defaults until you add `config/p-success-weights.json`.
|
|
365
|
+
|
|
366
|
+
### Community telemetry contribution (calibration)
|
|
367
|
+
|
|
368
|
+
When `SMART_ROUTER_DATASET=1`, you can export privacy-safe scalar routing features (plus outcome labels) for community calibration training. The export never includes prompt text, messages, raw session identifiers, or install-local pepper fields.
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
/smart-router export telemetry-contrib [--limit N]
|
|
372
|
+
# or from shell (cwd must contain .pi-smart-router/state.db):
|
|
373
|
+
npx pi-smart-router export telemetry-contrib [--limit N]
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
This writes schema-valid JSON to `.pi-smart-router/exports/telemetry-contrib-<timestamp>.json`. Each row conforms to [`telemetry-contrib.schema.json`](specs/001-build-smart-router/contracts/telemetry-contrib.schema.json).
|
|
377
|
+
|
|
378
|
+
**How to contribute**
|
|
379
|
+
|
|
380
|
+
1. Opt in to dataset capture (`SMART_ROUTER_DATASET=1`) and dogfood with `/model smart-router/auto` for several sessions.
|
|
381
|
+
2. Run `export telemetry-contrib` locally and review the export — confirm it contains no prompt content.
|
|
382
|
+
3. Submit anonymized rows via **pull request** under `data/contrib/` (one `.json` array or `.jsonl` file per install) **or** attach the export to a [GitHub Discussion](https://github.com/beettlle/pi-smart-router/discussions) using the community telemetry template.
|
|
383
|
+
4. Maintainers aggregate contributions with `npm run routing:calibration-aggregate -- --contrib-dir data/contrib`; ingest rejects tainted payloads (prompt/message keys) and strips install-local pepper fields before offline training (SP-116, SP-117).
|
|
384
|
+
|
|
385
|
+
See the synthetic reference file at [`data/contrib/example.json`](data/contrib/example.json).
|
|
386
|
+
|
|
387
|
+
### Operator tuning (frugality slider)
|
|
388
|
+
|
|
389
|
+
The multi-objective scoring weights control the cost-vs-quality tradeoff:
|
|
390
|
+
|
|
391
|
+
| Key | Default | Effect |
|
|
392
|
+
|-----|---------|--------|
|
|
393
|
+
| `frugality.lambda_cost` | 0.5 | Higher favors cheaper models at quality parity |
|
|
394
|
+
| `frugality.lambda_latency` | 0.1 | Higher penalizes slow models |
|
|
395
|
+
| `frugality.lambda_verbosity` | 0.15 | Higher penalizes verbose models |
|
|
396
|
+
|
|
397
|
+
Additional operator defaults:
|
|
398
|
+
|
|
399
|
+
| Key | Default | Purpose |
|
|
400
|
+
|-----|---------|---------|
|
|
401
|
+
| `loop_escalation.threshold` | 3 | Consecutive identical failures before escalating to frontier |
|
|
402
|
+
| `local.min_memory_gb_full` | 16 | Minimum RAM for full local inference |
|
|
403
|
+
| `local.battery_threshold_pct` | 20 | Minimum battery to allow local inference |
|
|
404
|
+
| `pricing.staleness_days` | 14 | Max age before re-fetching pricing data |
|
|
405
|
+
|
|
406
|
+
### HyDRA model cache
|
|
407
|
+
|
|
408
|
+
The embedding matcher uses `@huggingface/transformers` with the `Xenova/all-MiniLM-L6-v2` ONNX model (384-dim embeddings). Artifacts are downloaded at runtime and cached under `.pi-smart-router/models/` (configurable via `hydra.artifact_cache_path`). This directory is gitignored.
|
|
409
|
+
|
|
410
|
+
## Architecture
|
|
411
|
+
|
|
412
|
+
### Three execution tiers
|
|
413
|
+
|
|
414
|
+
| Tier | Catalog Name | Purpose | Example |
|
|
415
|
+
|------|-------------|---------|---------|
|
|
416
|
+
| Local | `zero-tier` | Free on-device inference for trivial tasks | Gemma via LM Studio |
|
|
417
|
+
| Cheap Cloud | `economical-cloud` | Budget API models for routine work | Claude Haiku |
|
|
418
|
+
| Frontier Cloud | `frontier-cloud` | Top-tier models for complex reasoning | Claude Sonnet |
|
|
419
|
+
|
|
420
|
+
### Pi extension
|
|
421
|
+
|
|
422
|
+
The pi integration path (npm install or project-local clone):
|
|
423
|
+
|
|
424
|
+
- Registers provider **`smart-router`** with model **`auto`**
|
|
425
|
+
- Implements **`streamSimple`** — runs the pipeline, resolves the target in `ModelRegistry`, delegates to the built-in streaming API for that provider
|
|
426
|
+
- Wires lifecycle hooks via `router.register()` for session state:
|
|
427
|
+
|
|
428
|
+
| Event | Purpose |
|
|
429
|
+
|-------|---------|
|
|
430
|
+
| `session_compact` / `session_before_compact` | Breaks session pin on compaction (via `LifecycleHookState`) |
|
|
431
|
+
| `model_select` | Records user-forced model overrides when `source === "set"` |
|
|
432
|
+
| `session_start` | Restores fleet mode from session entries |
|
|
433
|
+
|
|
434
|
+
Conversation context for routing is read from the stream delegation path (`buildRoutingRequest`), not from a library `context` hook. Library embedders supply `messages` / `prompt_text` when calling `dispatch.dispatch()`.
|
|
435
|
+
|
|
436
|
+
### Session pinning
|
|
437
|
+
|
|
438
|
+
Sessions pin to the first routed model to preserve provider-side prompt prefix caching. Pins break only on:
|
|
439
|
+
|
|
440
|
+
- Session compaction
|
|
441
|
+
- User model override (`/model` in pi)
|
|
442
|
+
- Loop escalation (repeated identical tool failures)
|
|
443
|
+
- Cache-warmup economics threshold
|
|
444
|
+
|
|
445
|
+
Sub-routing within a pin is allowed: small `tool_result` turns may use an economical model on the same provider without breaking the pin.
|
|
446
|
+
|
|
447
|
+
### Gateway resilience
|
|
448
|
+
|
|
449
|
+
The `GatewayDispatch` layer wraps the pipeline with:
|
|
450
|
+
|
|
451
|
+
- **Circuit breaker** — Per-model, tracks consecutive 5xx/network errors (CLOSED → OPEN → HALF_OPEN). 4xx and safety errors do not trip the breaker.
|
|
452
|
+
- **Failover chains** — On open circuit, routes to same-tier alternative via inverse-cost weighted selection.
|
|
453
|
+
- **Rate limiting** — Per-operator-key token bucket with `429 + Retry-After` responses.
|
|
454
|
+
|
|
455
|
+
### Troubleshooting
|
|
456
|
+
|
|
457
|
+
#### Gemini `thought_signature` 400 errors
|
|
458
|
+
|
|
459
|
+
If Gemini returns **400 INVALID_ARGUMENT** mentioning `thought_signature`, the router treats this as a **protocol validation error** (incomplete tool-call replay), not provider unavailability — it will **not** failover to another model.
|
|
460
|
+
|
|
461
|
+
This typically happens when a session with prior tool calls is routed to Gemini before [pi preserves thought signatures in replay](https://github.com/earendil-works/pi/issues/6342). See [Google's thought signatures documentation](https://ai.google.dev/gemini-api/docs/generate-content/thought-signatures).
|
|
462
|
+
|
|
463
|
+
**Automatic guard (SP-077):** sessions with prior tool-call history exclude Google/Gemini models from routing (telemetry `reason_code: gemini_tool_history_excluded`) unless the operator sets `force_model_id` via `/model`. Non-tool sessions are unchanged.
|
|
464
|
+
|
|
465
|
+
**Empty fleet fail-safe (SP-084):** when the guard filters every model in the scoped fleet (e.g. Google/Gemini-only dogfood configs), the router throws an actionable error instead of delegating with `selected_model_id: unknown`. Add a non-Google model such as `openai/gpt-4o-mini` or `cursor/auto` to the fleet, start `/new`, or pin `/model` to force a specific model.
|
|
466
|
+
|
|
467
|
+
**Workarounds:**
|
|
468
|
+
|
|
469
|
+
1. Start a fresh session with `/new` in pi.
|
|
470
|
+
2. Switch to a non-Google model (e.g. `/model openai/gpt-4o-mini`) for tool-heavy sessions until the upstream fix lands.
|
|
471
|
+
|
|
472
|
+
Related: [pi-smart-router#37](https://github.com/beettlle/pi-smart-router/issues/37), [pi-smart-router#38](https://github.com/beettlle/pi-smart-router/issues/38), [pi-smart-router#40](https://github.com/beettlle/pi-smart-router/issues/40), [pi-smart-router#41](https://github.com/beettlle/pi-smart-router/issues/41), [pi#6342](https://github.com/earendil-works/pi/issues/6342).
|
|
473
|
+
|
|
474
|
+
### Explain endpoint (library API)
|
|
475
|
+
|
|
476
|
+
The explain handler runs the identical pipeline but returns the `RoutingDecision` without dispatching upstream inference — guaranteeing bit-for-bit decision equivalence with the live path. Useful for debugging, operator trust, and shadow runs. Exposed via the library API (`src/api/explain/router-explain.ts`); HTTP/CLI wiring is embedder-specific.
|
|
477
|
+
|
|
478
|
+
## API
|
|
479
|
+
|
|
480
|
+
### Public exports
|
|
481
|
+
|
|
482
|
+
```typescript
|
|
483
|
+
import {
|
|
484
|
+
createRouter,
|
|
485
|
+
createRouterFromFleet,
|
|
486
|
+
createPiRouterMiddleware,
|
|
487
|
+
LifecycleHookState,
|
|
488
|
+
type RoutingDecision,
|
|
489
|
+
type ModelProfile,
|
|
490
|
+
type PiRouterMiddleware,
|
|
491
|
+
type PiExtensionHooks,
|
|
492
|
+
type RouterHandle,
|
|
493
|
+
} from 'pi-smart-router';
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
`createPiRouterMiddleware()` is exported for advanced embedders that need a standalone lifecycle hook registrar. Most callers should use `createRouter()` / `createRouterFromFleet()` and call `register()` on the returned handle.
|
|
497
|
+
|
|
498
|
+
### `RoutingDecision`
|
|
499
|
+
|
|
500
|
+
Every routing decision includes:
|
|
501
|
+
|
|
502
|
+
| Field | Type | Description |
|
|
503
|
+
|-------|------|-------------|
|
|
504
|
+
| `selected_model_id` | `string` | Fleet model ID chosen |
|
|
505
|
+
| `tier` | `Tier` | `zero-tier`, `economical-cloud`, or `frontier-cloud` |
|
|
506
|
+
| `stage` | `string` | Pipeline stage that decided (triage, session_pin, local_zero, etc.) |
|
|
507
|
+
| `reason_code` | `string` | Machine-readable reason |
|
|
508
|
+
| `candidates` | `string[]` | Models considered before selection |
|
|
509
|
+
| `estimated_cost_usd` | `number` | Per-request cost estimate |
|
|
510
|
+
| `routing_latency_ms` | `number` | Time spent in the routing pipeline |
|
|
511
|
+
|
|
512
|
+
## Development
|
|
513
|
+
|
|
514
|
+
```bash
|
|
515
|
+
git clone https://github.com/beettlle/pi-smart-router.git
|
|
516
|
+
cd pi-smart-router
|
|
517
|
+
npm install
|
|
518
|
+
npm run build
|
|
519
|
+
npm run verify:ci
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
Contributors must run `npm run build` before publishing or consuming the library API from `dist/`. The pi extension uses TypeScript source directly (via pi's jiti loader) and does not require a local build for clone-based dogfooding.
|
|
523
|
+
|
|
524
|
+
### Scripts
|
|
525
|
+
|
|
526
|
+
| Script | Purpose |
|
|
527
|
+
|--------|---------|
|
|
528
|
+
| `npm run build` | Compile library to `dist/` (`tsc --project tsconfig.build.json`) |
|
|
529
|
+
| `npm run release:check` | Pre-release gate (same as `verify:ci`) |
|
|
530
|
+
| `npm run verify:ci` | Full CI parity: build, typecheck, lint, test, coverage |
|
|
531
|
+
| `npm run typecheck` | TypeScript strict mode check (`tsc --noEmit`) |
|
|
532
|
+
| `npm test` | Run test suite (`vitest run`) |
|
|
533
|
+
| `npm run coverage:check` | Tests with line-coverage thresholds |
|
|
534
|
+
| `npm run lint` | ESLint + fleet catalog validation |
|
|
535
|
+
| `npm run routing:bootstrap-centroids` | Regenerate `config/routing-centroids.json` from cluster catalog |
|
|
536
|
+
| `npm run routing:calibration-aggregate` | Aggregate community telemetry for calibration |
|
|
537
|
+
| `npm run routing:train-calibration` | Train routing calibration artifact bundle |
|
|
538
|
+
| `npm run routing:verify-calibration` | Verify calibration bundle against benchmark prompts |
|
|
539
|
+
|
|
540
|
+
### Releasing
|
|
541
|
+
|
|
542
|
+
Tag-triggered publish via GitHub Actions (requires `NPMSECRET` repository secret):
|
|
543
|
+
|
|
544
|
+
1. `npm run release:check`
|
|
545
|
+
2. `npm version 0.1.0` (creates commit + `v0.1.0` tag)
|
|
546
|
+
3. `git push && git push --tags`
|
|
547
|
+
4. Actions → **Release** runs `npm publish` and creates a GitHub Release
|
|
548
|
+
5. Post-publish smoke: `pi install npm:pi-smart-router` and `/model smart-router/auto`
|
|
549
|
+
|
|
550
|
+
Re-publish a failed release: Actions → Release → Run workflow with existing tag (e.g. `v0.1.0`).
|
|
551
|
+
|
|
552
|
+
Dry-run tarball contents locally:
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
npm run release:check
|
|
556
|
+
npm pack --dry-run
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Test suite
|
|
560
|
+
|
|
561
|
+
1117 tests across 61 test files covering:
|
|
562
|
+
|
|
563
|
+
- Unit tests for every pipeline stage, domain module, and infrastructure component
|
|
564
|
+
- Contract tests validating routing request/decision schemas
|
|
565
|
+
- Integration tests for full pipeline routing, session pinning, latency budgets, and cost baselines
|
|
566
|
+
- Pi extension tests (`tests/integration/pi-extension.test.ts`) for registry → fleet → stream delegation
|
|
567
|
+
- Resilience tests for circuit breaker, failover, and rate limiting
|
|
568
|
+
|
|
569
|
+
## Documentation
|
|
570
|
+
|
|
571
|
+
| Document | Purpose |
|
|
572
|
+
|----------|---------|
|
|
573
|
+
| [docs/PRD.md](docs/PRD.md) | Product requirements, research lineage, pipeline specification |
|
|
574
|
+
| [docs/constitution.md](docs/constitution.md) | Project principles and non-negotiable rules |
|
|
575
|
+
| [specs/001-build-smart-router/spec.md](specs/001-build-smart-router/spec.md) | Detailed feature specification |
|
|
576
|
+
| [specs/001-build-smart-router/data-model.md](specs/001-build-smart-router/data-model.md) | Entity definitions, schemas, configuration reference |
|
|
577
|
+
| [specs/001-build-smart-router/quickstart.md](specs/001-build-smart-router/quickstart.md) | Setup and verification guide |
|
|
578
|
+
| [config/models.yaml.example](config/models.yaml.example) | Fleet catalog template (library API) |
|
|
579
|
+
| [config/routing-clusters.yaml.example](config/routing-clusters.yaml.example) | Routing cluster reference-prompt catalog (library API) |
|
|
580
|
+
|
|
581
|
+
## Built with
|
|
582
|
+
|
|
583
|
+
- [pi](https://pi.dev) — Coding agent harness (extension host)
|
|
584
|
+
- [@earendil-works/pi-ai](https://pi.dev) — Provider streaming APIs
|
|
585
|
+
- [pi-spine](https://github.com/beettlle/pi-spine) — Batch orchestration (used to build this project)
|
|
586
|
+
- [stet](https://github.com/beettlle/stet) — Local code review (guardrails during development)
|
|
587
|
+
- [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) — Shared state store
|
|
588
|
+
- [zod](https://zod.dev) — Runtime schema validation
|
|
589
|
+
- [@huggingface/transformers](https://huggingface.co/docs/transformers.js) — ONNX embedding inference for HyDRA matcher
|
|
590
|
+
|
|
591
|
+
## License
|
|
592
|
+
|
|
593
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pi-smart-router CLI — offline operator tools (telemetry export, etc.).
|
|
5
|
+
* Requires `npm run build` so dist/ artifacts exist (release:check runs build first).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const MIN_NODE_MAJOR = 22;
|
|
12
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] ?? '0', 10);
|
|
13
|
+
if (nodeMajor < MIN_NODE_MAJOR) {
|
|
14
|
+
console.error(
|
|
15
|
+
`pi-smart-router requires Node.js >= ${MIN_NODE_MAJOR}.0.0 (found ${process.versions.node}).`,
|
|
16
|
+
);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
|
|
22
|
+
async function loadDist(modulePath) {
|
|
23
|
+
return import(pathToFileURL(join(packageRoot, modulePath)).href);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getRouterStateDbPath(cwd) {
|
|
27
|
+
const configured = process.env.ROUTER_STATE_DB_PATH?.trim();
|
|
28
|
+
if (configured && configured.length > 0) {
|
|
29
|
+
return configured;
|
|
30
|
+
}
|
|
31
|
+
return join(cwd, '.pi-smart-router/state.db');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function printUsage() {
|
|
35
|
+
console.error(`Usage:
|
|
36
|
+
pi-smart-router export telemetry-contrib [--limit N]
|
|
37
|
+
|
|
38
|
+
Run from a directory with pi routing state (default: ./.pi-smart-router/state.db).`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function main() {
|
|
42
|
+
const argv = process.argv.slice(2);
|
|
43
|
+
const args = argv.join(' ');
|
|
44
|
+
|
|
45
|
+
const cli = await loadDist('dist/cli/smart-router-cli.js');
|
|
46
|
+
const sqlite = await loadDist('dist/infrastructure/persistence/sqlite-store.js');
|
|
47
|
+
|
|
48
|
+
if (cli.isExportTelemetryContribInvocation(args)) {
|
|
49
|
+
const { limit } = cli.parseExportTelemetryContribArgs(args);
|
|
50
|
+
const cwd = process.cwd();
|
|
51
|
+
const { store } = sqlite.createResilientStore({
|
|
52
|
+
dbPath: getRouterStateDbPath(cwd),
|
|
53
|
+
models: [],
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await cli.exportTelemetryContrib({ store, cwd, limit });
|
|
57
|
+
if (result.path) {
|
|
58
|
+
console.log(`Exported ${result.recordCount} telemetry-contrib record(s) to ${result.path}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (result.recordCount === 0) {
|
|
63
|
+
console.log('No telemetry-contrib records to export (opt in with SMART_ROUTER_DATASET=1).');
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(result.json);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
printUsage();
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
main().catch((error) => {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
console.error(message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
});
|
package/config/.gitkeep
ADDED
|
File without changes
|