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.
Files changed (310) hide show
  1. package/.pi/extensions/smart-router/command-formatters.ts +167 -0
  2. package/.pi/extensions/smart-router/commands.ts +242 -0
  3. package/.pi/extensions/smart-router/dataset-export.ts +142 -0
  4. package/.pi/extensions/smart-router/delegate-stream.ts +105 -0
  5. package/.pi/extensions/smart-router/delegation-runtime.ts +355 -0
  6. package/.pi/extensions/smart-router/extension-setup.ts +115 -0
  7. package/.pi/extensions/smart-router/fleet-bootstrap.ts +244 -0
  8. package/.pi/extensions/smart-router/index.ts +115 -0
  9. package/.pi/extensions/smart-router/package.json +3 -0
  10. package/.pi/extensions/smart-router/pi-model-scope.ts +55 -0
  11. package/.pi/extensions/smart-router/pricing-lifecycle.ts +43 -0
  12. package/.pi/extensions/smart-router/route-and-delegate.ts +562 -0
  13. package/.pi/extensions/smart-router/routing-context.ts +171 -0
  14. package/.pi/extensions/smart-router/routing-outcomes.ts +44 -0
  15. package/.pi/extensions/smart-router/session-lifecycle.ts +140 -0
  16. package/.pi/extensions/smart-router/stream-delegation.ts +75 -0
  17. package/.pi/extensions/smart-router/types.ts +90 -0
  18. package/.pi/extensions/smart-router/utils.ts +58 -0
  19. package/README.md +593 -0
  20. package/bin/pi-smart-router.mjs +79 -0
  21. package/config/.gitkeep +0 -0
  22. package/config/hydra-projection-weights.json.example +1169 -0
  23. package/config/models.yaml.example +58 -0
  24. package/config/p-success-weights.json.example +19 -0
  25. package/config/routing-calibration.json.example +1605 -0
  26. package/config/routing-centroids.json.example +1570 -0
  27. package/config/routing-clusters.yaml.example +41 -0
  28. package/data/contrib/example.json +25 -0
  29. package/dist/api/explain/router-explain.d.ts +47 -0
  30. package/dist/api/explain/router-explain.d.ts.map +1 -0
  31. package/dist/api/explain/router-explain.js +101 -0
  32. package/dist/api/explain/router-explain.js.map +1 -0
  33. package/dist/api/middleware/pi-router-middleware.d.ts +86 -0
  34. package/dist/api/middleware/pi-router-middleware.d.ts.map +1 -0
  35. package/dist/api/middleware/pi-router-middleware.js +83 -0
  36. package/dist/api/middleware/pi-router-middleware.js.map +1 -0
  37. package/dist/cli/smart-router-cli.d.ts +85 -0
  38. package/dist/cli/smart-router-cli.d.ts.map +1 -0
  39. package/dist/cli/smart-router-cli.js +293 -0
  40. package/dist/cli/smart-router-cli.js.map +1 -0
  41. package/dist/config/defaults.d.ts +7 -0
  42. package/dist/config/defaults.d.ts.map +1 -0
  43. package/dist/config/defaults.js +33 -0
  44. package/dist/config/defaults.js.map +1 -0
  45. package/dist/config/models-loader.d.ts +22 -0
  46. package/dist/config/models-loader.d.ts.map +1 -0
  47. package/dist/config/models-loader.js +51 -0
  48. package/dist/config/models-loader.js.map +1 -0
  49. package/dist/config/pi-model-mapper.d.ts +40 -0
  50. package/dist/config/pi-model-mapper.d.ts.map +1 -0
  51. package/dist/config/pi-model-mapper.js +216 -0
  52. package/dist/config/pi-model-mapper.js.map +1 -0
  53. package/dist/config/routing-clusters-loader.d.ts +35 -0
  54. package/dist/config/routing-clusters-loader.d.ts.map +1 -0
  55. package/dist/config/routing-clusters-loader.js +100 -0
  56. package/dist/config/routing-clusters-loader.js.map +1 -0
  57. package/dist/domain/delegation/delegation-context.d.ts +23 -0
  58. package/dist/domain/delegation/delegation-context.d.ts.map +1 -0
  59. package/dist/domain/delegation/delegation-context.js +73 -0
  60. package/dist/domain/delegation/delegation-context.js.map +1 -0
  61. package/dist/domain/delegation/execution-ledger.d.ts +19 -0
  62. package/dist/domain/delegation/execution-ledger.d.ts.map +1 -0
  63. package/dist/domain/delegation/execution-ledger.js +23 -0
  64. package/dist/domain/delegation/execution-ledger.js.map +1 -0
  65. package/dist/domain/delegation/output-headroom.d.ts +39 -0
  66. package/dist/domain/delegation/output-headroom.d.ts.map +1 -0
  67. package/dist/domain/delegation/output-headroom.js +78 -0
  68. package/dist/domain/delegation/output-headroom.js.map +1 -0
  69. package/dist/domain/matching/cluster-matcher.d.ts +69 -0
  70. package/dist/domain/matching/cluster-matcher.d.ts.map +1 -0
  71. package/dist/domain/matching/cluster-matcher.js +294 -0
  72. package/dist/domain/matching/cluster-matcher.js.map +1 -0
  73. package/dist/domain/matching/embedding-provider.d.ts +21 -0
  74. package/dist/domain/matching/embedding-provider.d.ts.map +1 -0
  75. package/dist/domain/matching/embedding-provider.js +42 -0
  76. package/dist/domain/matching/embedding-provider.js.map +1 -0
  77. package/dist/domain/matching/hydra-input.d.ts +18 -0
  78. package/dist/domain/matching/hydra-input.d.ts.map +1 -0
  79. package/dist/domain/matching/hydra-input.js +40 -0
  80. package/dist/domain/matching/hydra-input.js.map +1 -0
  81. package/dist/domain/matching/hydra-matcher.d.ts +103 -0
  82. package/dist/domain/matching/hydra-matcher.d.ts.map +1 -0
  83. package/dist/domain/matching/hydra-matcher.js +275 -0
  84. package/dist/domain/matching/hydra-matcher.js.map +1 -0
  85. package/dist/domain/pinning/cache-economics.d.ts +53 -0
  86. package/dist/domain/pinning/cache-economics.d.ts.map +1 -0
  87. package/dist/domain/pinning/cache-economics.js +72 -0
  88. package/dist/domain/pinning/cache-economics.js.map +1 -0
  89. package/dist/domain/pinning/loop-escalation.d.ts +41 -0
  90. package/dist/domain/pinning/loop-escalation.d.ts.map +1 -0
  91. package/dist/domain/pinning/loop-escalation.js +137 -0
  92. package/dist/domain/pinning/loop-escalation.js.map +1 -0
  93. package/dist/domain/pinning/session-pinner.d.ts +81 -0
  94. package/dist/domain/pinning/session-pinner.d.ts.map +1 -0
  95. package/dist/domain/pinning/session-pinner.js +237 -0
  96. package/dist/domain/pinning/session-pinner.js.map +1 -0
  97. package/dist/domain/pinning/sub-route-policy.d.ts +41 -0
  98. package/dist/domain/pinning/sub-route-policy.d.ts.map +1 -0
  99. package/dist/domain/pinning/sub-route-policy.js +72 -0
  100. package/dist/domain/pinning/sub-route-policy.js.map +1 -0
  101. package/dist/domain/pipeline/router-pipeline.d.ts +180 -0
  102. package/dist/domain/pipeline/router-pipeline.d.ts.map +1 -0
  103. package/dist/domain/pipeline/router-pipeline.js +807 -0
  104. package/dist/domain/pipeline/router-pipeline.js.map +1 -0
  105. package/dist/domain/pipeline/safe-default.d.ts +25 -0
  106. package/dist/domain/pipeline/safe-default.d.ts.map +1 -0
  107. package/dist/domain/pipeline/safe-default.js +41 -0
  108. package/dist/domain/pipeline/safe-default.js.map +1 -0
  109. package/dist/domain/refactor-guardrails.d.ts +48 -0
  110. package/dist/domain/refactor-guardrails.d.ts.map +1 -0
  111. package/dist/domain/refactor-guardrails.js +129 -0
  112. package/dist/domain/refactor-guardrails.js.map +1 -0
  113. package/dist/domain/routing/context-fit.d.ts +53 -0
  114. package/dist/domain/routing/context-fit.d.ts.map +1 -0
  115. package/dist/domain/routing/context-fit.js +142 -0
  116. package/dist/domain/routing/context-fit.js.map +1 -0
  117. package/dist/domain/routing/expected-cost.d.ts +63 -0
  118. package/dist/domain/routing/expected-cost.d.ts.map +1 -0
  119. package/dist/domain/routing/expected-cost.js +177 -0
  120. package/dist/domain/routing/expected-cost.js.map +1 -0
  121. package/dist/domain/routing/p-success-classifier.d.ts +130 -0
  122. package/dist/domain/routing/p-success-classifier.d.ts.map +1 -0
  123. package/dist/domain/routing/p-success-classifier.js +350 -0
  124. package/dist/domain/routing/p-success-classifier.js.map +1 -0
  125. package/dist/domain/routing/tier-features.d.ts +70 -0
  126. package/dist/domain/routing/tier-features.d.ts.map +1 -0
  127. package/dist/domain/routing/tier-features.js +174 -0
  128. package/dist/domain/routing/tier-features.js.map +1 -0
  129. package/dist/domain/routing/tool-history-guard.d.ts +32 -0
  130. package/dist/domain/routing/tool-history-guard.d.ts.map +1 -0
  131. package/dist/domain/routing/tool-history-guard.js +110 -0
  132. package/dist/domain/routing/tool-history-guard.js.map +1 -0
  133. package/dist/domain/scoring/multi-objective.d.ts +48 -0
  134. package/dist/domain/scoring/multi-objective.d.ts.map +1 -0
  135. package/dist/domain/scoring/multi-objective.js +116 -0
  136. package/dist/domain/scoring/multi-objective.js.map +1 -0
  137. package/dist/domain/triage/triage-engine.d.ts +43 -0
  138. package/dist/domain/triage/triage-engine.d.ts.map +1 -0
  139. package/dist/domain/triage/triage-engine.js +317 -0
  140. package/dist/domain/triage/triage-engine.js.map +1 -0
  141. package/dist/domain/triage/turn-envelope.d.ts +17 -0
  142. package/dist/domain/triage/turn-envelope.d.ts.map +1 -0
  143. package/dist/domain/triage/turn-envelope.js +81 -0
  144. package/dist/domain/triage/turn-envelope.js.map +1 -0
  145. package/dist/domain/types/entities.d.ts +303 -0
  146. package/dist/domain/types/entities.d.ts.map +1 -0
  147. package/dist/domain/types/entities.js +6 -0
  148. package/dist/domain/types/entities.js.map +1 -0
  149. package/dist/domain/types/index.d.ts +4 -0
  150. package/dist/domain/types/index.d.ts.map +1 -0
  151. package/dist/domain/types/index.js +2 -0
  152. package/dist/domain/types/index.js.map +1 -0
  153. package/dist/domain/types/schemas.d.ts +346 -0
  154. package/dist/domain/types/schemas.d.ts.map +1 -0
  155. package/dist/domain/types/schemas.js +241 -0
  156. package/dist/domain/types/schemas.js.map +1 -0
  157. package/dist/domain/types/store-port.d.ts +44 -0
  158. package/dist/domain/types/store-port.d.ts.map +1 -0
  159. package/dist/domain/types/store-port.js +6 -0
  160. package/dist/domain/types/store-port.js.map +1 -0
  161. package/dist/index.d.ts +39 -0
  162. package/dist/index.d.ts.map +1 -0
  163. package/dist/index.js +36 -0
  164. package/dist/index.js.map +1 -0
  165. package/dist/infra/gemini-provider.d.ts +28 -0
  166. package/dist/infra/gemini-provider.d.ts.map +1 -0
  167. package/dist/infra/gemini-provider.js +112 -0
  168. package/dist/infra/gemini-provider.js.map +1 -0
  169. package/dist/infra/telemetry.d.ts +51 -0
  170. package/dist/infra/telemetry.d.ts.map +1 -0
  171. package/dist/infra/telemetry.js +100 -0
  172. package/dist/infra/telemetry.js.map +1 -0
  173. package/dist/infrastructure/delegation/provider-error.d.ts +44 -0
  174. package/dist/infrastructure/delegation/provider-error.d.ts.map +1 -0
  175. package/dist/infrastructure/delegation/provider-error.js +179 -0
  176. package/dist/infrastructure/delegation/provider-error.js.map +1 -0
  177. package/dist/infrastructure/gateway/circuit-breaker.d.ts +68 -0
  178. package/dist/infrastructure/gateway/circuit-breaker.d.ts.map +1 -0
  179. package/dist/infrastructure/gateway/circuit-breaker.js +150 -0
  180. package/dist/infrastructure/gateway/circuit-breaker.js.map +1 -0
  181. package/dist/infrastructure/gateway/gateway-dispatch.d.ts +138 -0
  182. package/dist/infrastructure/gateway/gateway-dispatch.d.ts.map +1 -0
  183. package/dist/infrastructure/gateway/gateway-dispatch.js +308 -0
  184. package/dist/infrastructure/gateway/gateway-dispatch.js.map +1 -0
  185. package/dist/infrastructure/hardware/hardware-probe.d.ts +32 -0
  186. package/dist/infrastructure/hardware/hardware-probe.d.ts.map +1 -0
  187. package/dist/infrastructure/hardware/hardware-probe.js +199 -0
  188. package/dist/infrastructure/hardware/hardware-probe.js.map +1 -0
  189. package/dist/infrastructure/local/local-zero-tier.d.ts +47 -0
  190. package/dist/infrastructure/local/local-zero-tier.d.ts.map +1 -0
  191. package/dist/infrastructure/local/local-zero-tier.js +92 -0
  192. package/dist/infrastructure/local/local-zero-tier.js.map +1 -0
  193. package/dist/infrastructure/persistence/memory-store.d.ts +31 -0
  194. package/dist/infrastructure/persistence/memory-store.d.ts.map +1 -0
  195. package/dist/infrastructure/persistence/memory-store.js +87 -0
  196. package/dist/infrastructure/persistence/memory-store.js.map +1 -0
  197. package/dist/infrastructure/persistence/sqlite-store.d.ts +81 -0
  198. package/dist/infrastructure/persistence/sqlite-store.d.ts.map +1 -0
  199. package/dist/infrastructure/persistence/sqlite-store.js +658 -0
  200. package/dist/infrastructure/persistence/sqlite-store.js.map +1 -0
  201. package/dist/infrastructure/pricing/litellm-fetch.d.ts +46 -0
  202. package/dist/infrastructure/pricing/litellm-fetch.d.ts.map +1 -0
  203. package/dist/infrastructure/pricing/litellm-fetch.js +145 -0
  204. package/dist/infrastructure/pricing/litellm-fetch.js.map +1 -0
  205. package/dist/infrastructure/pricing/price-broker.d.ts +56 -0
  206. package/dist/infrastructure/pricing/price-broker.d.ts.map +1 -0
  207. package/dist/infrastructure/pricing/price-broker.js +133 -0
  208. package/dist/infrastructure/pricing/price-broker.js.map +1 -0
  209. package/dist/infrastructure/pricing/pricing-monitor.d.ts +27 -0
  210. package/dist/infrastructure/pricing/pricing-monitor.d.ts.map +1 -0
  211. package/dist/infrastructure/pricing/pricing-monitor.js +46 -0
  212. package/dist/infrastructure/pricing/pricing-monitor.js.map +1 -0
  213. package/dist/infrastructure/telemetry/dataset-limits.d.ts +13 -0
  214. package/dist/infrastructure/telemetry/dataset-limits.d.ts.map +1 -0
  215. package/dist/infrastructure/telemetry/dataset-limits.js +28 -0
  216. package/dist/infrastructure/telemetry/dataset-limits.js.map +1 -0
  217. package/dist/infrastructure/telemetry/dataset-recorder.d.ts +48 -0
  218. package/dist/infrastructure/telemetry/dataset-recorder.d.ts.map +1 -0
  219. package/dist/infrastructure/telemetry/dataset-recorder.js +154 -0
  220. package/dist/infrastructure/telemetry/dataset-recorder.js.map +1 -0
  221. package/dist/infrastructure/telemetry/outcome-limits.d.ts +12 -0
  222. package/dist/infrastructure/telemetry/outcome-limits.d.ts.map +1 -0
  223. package/dist/infrastructure/telemetry/outcome-limits.js +28 -0
  224. package/dist/infrastructure/telemetry/outcome-limits.js.map +1 -0
  225. package/dist/infrastructure/telemetry/outcome-recorder.d.ts +29 -0
  226. package/dist/infrastructure/telemetry/outcome-recorder.d.ts.map +1 -0
  227. package/dist/infrastructure/telemetry/outcome-recorder.js +49 -0
  228. package/dist/infrastructure/telemetry/outcome-recorder.js.map +1 -0
  229. package/dist/infrastructure/telemetry/routing-telemetry.d.ts +115 -0
  230. package/dist/infrastructure/telemetry/routing-telemetry.d.ts.map +1 -0
  231. package/dist/infrastructure/telemetry/routing-telemetry.js +529 -0
  232. package/dist/infrastructure/telemetry/routing-telemetry.js.map +1 -0
  233. package/dist/infrastructure/telemetry/telemetry-limits.d.ts +16 -0
  234. package/dist/infrastructure/telemetry/telemetry-limits.d.ts.map +1 -0
  235. package/dist/infrastructure/telemetry/telemetry-limits.js +35 -0
  236. package/dist/infrastructure/telemetry/telemetry-limits.js.map +1 -0
  237. package/package.json +110 -0
  238. package/skills/router-backlog-orchestrator/SKILL.md +183 -0
  239. package/skills/router-backlog-orchestrator/references/github-router-issue-template.md +78 -0
  240. package/skills/router-backlog-orchestrator/references/packet-from-issue.md +99 -0
  241. package/skills/router-backlog-orchestrator/references/prioritization-rubric.md +65 -0
  242. package/skills/router-backlog-orchestrator/scripts/collect-backlog.sh +44 -0
  243. package/specs/001-build-smart-router/contracts/telemetry-contrib.schema.json +151 -0
  244. package/src/api/explain/.gitkeep +0 -0
  245. package/src/api/explain/router-explain.ts +146 -0
  246. package/src/api/middleware/.gitkeep +0 -0
  247. package/src/api/middleware/pi-router-middleware.ts +169 -0
  248. package/src/cli/smart-router-cli.ts +436 -0
  249. package/src/config/.gitkeep +0 -0
  250. package/src/config/defaults.ts +35 -0
  251. package/src/config/models-loader.ts +68 -0
  252. package/src/config/pi-model-mapper.ts +285 -0
  253. package/src/config/routing-clusters-loader.ts +148 -0
  254. package/src/domain/delegation/delegation-context.ts +111 -0
  255. package/src/domain/delegation/execution-ledger.ts +34 -0
  256. package/src/domain/delegation/output-headroom.ts +131 -0
  257. package/src/domain/matching/.gitkeep +0 -0
  258. package/src/domain/matching/cluster-matcher.ts +474 -0
  259. package/src/domain/matching/embedding-provider.ts +79 -0
  260. package/src/domain/matching/hydra-input.ts +53 -0
  261. package/src/domain/matching/hydra-matcher.ts +454 -0
  262. package/src/domain/pinning/.gitkeep +0 -0
  263. package/src/domain/pinning/cache-economics.ts +128 -0
  264. package/src/domain/pinning/loop-escalation.ts +181 -0
  265. package/src/domain/pinning/session-pinner.ts +388 -0
  266. package/src/domain/pinning/sub-route-policy.ts +119 -0
  267. package/src/domain/pipeline/.gitkeep +0 -0
  268. package/src/domain/pipeline/router-pipeline.ts +1139 -0
  269. package/src/domain/pipeline/safe-default.ts +70 -0
  270. package/src/domain/refactor-guardrails.ts +202 -0
  271. package/src/domain/routing/context-fit.ts +221 -0
  272. package/src/domain/routing/expected-cost.ts +320 -0
  273. package/src/domain/routing/p-success-classifier.ts +511 -0
  274. package/src/domain/routing/tier-features.ts +290 -0
  275. package/src/domain/routing/tool-history-guard.ts +159 -0
  276. package/src/domain/scoring/.gitkeep +0 -0
  277. package/src/domain/scoring/multi-objective.ts +175 -0
  278. package/src/domain/triage/.gitkeep +0 -0
  279. package/src/domain/triage/triage-engine.ts +373 -0
  280. package/src/domain/triage/turn-envelope.ts +104 -0
  281. package/src/domain/types/.gitkeep +0 -0
  282. package/src/domain/types/entities.ts +384 -0
  283. package/src/domain/types/index.ts +41 -0
  284. package/src/domain/types/schemas.ts +289 -0
  285. package/src/domain/types/store-port.ts +59 -0
  286. package/src/index.ts +103 -0
  287. package/src/infra/gemini-provider.ts +142 -0
  288. package/src/infra/telemetry.ts +169 -0
  289. package/src/infrastructure/delegation/provider-error.ts +251 -0
  290. package/src/infrastructure/gateway/.gitkeep +0 -0
  291. package/src/infrastructure/gateway/circuit-breaker.ts +196 -0
  292. package/src/infrastructure/gateway/gateway-dispatch.ts +444 -0
  293. package/src/infrastructure/hardware/.gitkeep +0 -0
  294. package/src/infrastructure/hardware/hardware-probe.ts +276 -0
  295. package/src/infrastructure/local/.gitkeep +0 -0
  296. package/src/infrastructure/local/local-zero-tier.ts +151 -0
  297. package/src/infrastructure/persistence/.gitkeep +0 -0
  298. package/src/infrastructure/persistence/memory-store.ts +122 -0
  299. package/src/infrastructure/persistence/sqlite-store.ts +900 -0
  300. package/src/infrastructure/pricing/.gitkeep +0 -0
  301. package/src/infrastructure/pricing/litellm-fetch.ts +215 -0
  302. package/src/infrastructure/pricing/price-broker.ts +183 -0
  303. package/src/infrastructure/pricing/pricing-monitor.ts +65 -0
  304. package/src/infrastructure/telemetry/.gitkeep +0 -0
  305. package/src/infrastructure/telemetry/dataset-limits.ts +39 -0
  306. package/src/infrastructure/telemetry/dataset-recorder.ts +215 -0
  307. package/src/infrastructure/telemetry/outcome-limits.ts +42 -0
  308. package/src/infrastructure/telemetry/outcome-recorder.ts +118 -0
  309. package/src/infrastructure/telemetry/routing-telemetry.ts +824 -0
  310. 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
+ });
File without changes