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
@@ -0,0 +1,151 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://pi-smart-router.dev/contracts/telemetry-contrib.schema.json",
4
+ "title": "TelemetryContribRecord",
5
+ "description": "Privacy-safe community telemetry contribution row (GitHub #67). Scalar routing features and outcome labels only — never prompt text, messages, request_id, or install-local pepper fields.",
6
+ "type": "object",
7
+ "required": [
8
+ "version",
9
+ "timestamp",
10
+ "session_id_hash",
11
+ "turn_type",
12
+ "reason_code",
13
+ "selected_model_id",
14
+ "routing_latency_ms"
15
+ ],
16
+ "additionalProperties": false,
17
+ "properties": {
18
+ "version": {
19
+ "type": "integer",
20
+ "const": 1,
21
+ "description": "Contrib record format version."
22
+ },
23
+ "timestamp": {
24
+ "type": "string",
25
+ "format": "date-time",
26
+ "description": "ISO-8601 routing decision timestamp."
27
+ },
28
+ "session_id_hash": {
29
+ "type": "string",
30
+ "pattern": "^[a-f0-9]{64}$",
31
+ "description": "One-way SHA-256 hash of install-local session_id for correlation without export."
32
+ },
33
+ "turn_type": {
34
+ "type": "string",
35
+ "minLength": 1
36
+ },
37
+ "stage": {
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "reason_code": {
42
+ "type": "string",
43
+ "minLength": 1
44
+ },
45
+ "selected_model_id": {
46
+ "type": "string",
47
+ "minLength": 1
48
+ },
49
+ "tier": {
50
+ "type": "string",
51
+ "enum": ["zero-tier", "economical-cloud", "frontier-cloud"]
52
+ },
53
+ "routing_latency_ms": {
54
+ "type": "number",
55
+ "minimum": 0
56
+ },
57
+ "estimated_cost_usd": {
58
+ "type": ["number", "null"],
59
+ "minimum": 0
60
+ },
61
+ "estimated_input_tokens": {
62
+ "type": ["integer", "null"],
63
+ "minimum": 0
64
+ },
65
+ "has_tool_context": {
66
+ "type": "boolean"
67
+ },
68
+ "compaction_flag": {
69
+ "type": "boolean"
70
+ },
71
+ "triage_verdict": {
72
+ "type": ["string", "null"]
73
+ },
74
+ "triage_reason_code": {
75
+ "type": ["string", "null"]
76
+ },
77
+ "triage_cyclomatic_score": {
78
+ "type": ["number", "null"],
79
+ "minimum": 0
80
+ },
81
+ "triage_trivial_hits": {
82
+ "type": ["integer", "null"],
83
+ "minimum": 0
84
+ },
85
+ "triage_complex_hits": {
86
+ "type": ["integer", "null"],
87
+ "minimum": 0
88
+ },
89
+ "triage_sanitized_length_delta": {
90
+ "type": ["integer", "null"]
91
+ },
92
+ "requirement_reasoning": {
93
+ "type": ["number", "null"],
94
+ "minimum": 0,
95
+ "maximum": 1
96
+ },
97
+ "requirement_code_gen": {
98
+ "type": ["number", "null"],
99
+ "minimum": 0,
100
+ "maximum": 1
101
+ },
102
+ "requirement_tool_use": {
103
+ "type": ["number", "null"],
104
+ "minimum": 0,
105
+ "maximum": 1
106
+ },
107
+ "cluster_id": {
108
+ "type": ["string", "null"]
109
+ },
110
+ "cluster_similarity": {
111
+ "type": ["number", "null"]
112
+ },
113
+ "cluster_margin": {
114
+ "type": ["number", "null"]
115
+ },
116
+ "low_intensity_score": {
117
+ "type": ["number", "null"]
118
+ },
119
+ "tier_hint": {
120
+ "type": ["string", "null"],
121
+ "enum": ["zero-tier", "economical-cloud", "frontier-cloud", null]
122
+ },
123
+ "p_success_cheap": {
124
+ "type": ["number", "null"],
125
+ "minimum": 0,
126
+ "maximum": 1
127
+ },
128
+ "local_eligible_reason": {
129
+ "type": ["string", "null"]
130
+ },
131
+ "tier_selection_reason_code": {
132
+ "type": ["string", "null"]
133
+ },
134
+ "success_label": {
135
+ "type": ["boolean", "null"],
136
+ "description": "Derived outcome label when behavioral signals exist."
137
+ },
138
+ "outcome_signals": {
139
+ "type": "array",
140
+ "items": {
141
+ "type": "string",
142
+ "enum": [
143
+ "model_override",
144
+ "compaction_pin_break",
145
+ "feedback_good",
146
+ "feedback_bad"
147
+ ]
148
+ }
149
+ }
150
+ }
151
+ }
File without changes
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Explain endpoint handler — T041.
3
+ *
4
+ * Returns routing rationale (tier, stage, reason_code, candidates) without
5
+ * dispatching upstream inference. Runs the same pipeline as the live path
6
+ * to guarantee bit-for-bit decision equivalence.
7
+ *
8
+ * Contract: specs/001-build-smart-router/contracts/explain-endpoint.md v1.0.0
9
+ *
10
+ * Invariants:
11
+ * - MUST NOT call upstream LLM providers
12
+ * - MUST produce a decision identical to the live pipeline for the same input
13
+ * - MUST emit no RoutingTelemetry with upstream cost (routing_latency_ms only)
14
+ */
15
+
16
+ import type { ModelProfile, RoutingDecision, RoutingRequest, Message } from '../../domain/types/index.js';
17
+ import { RoutingRequestSchema } from '../../domain/types/schemas.js';
18
+ import { RouterPipeline } from '../../domain/pipeline/router-pipeline.js';
19
+ import { safeCloudDefault } from '../../domain/pipeline/safe-default.js';
20
+ import type { ClusterMatcher } from '../../domain/matching/cluster-matcher.js';
21
+ import { enrichRoutingDecisionForExplain } from '../../infrastructure/telemetry/routing-telemetry.js';
22
+
23
+ // ─── Result types ─────────────────────────────────────────────────────────────
24
+
25
+ export interface ExplainValidationError {
26
+ readonly error: 'validation_failed';
27
+ readonly details: readonly string[];
28
+ }
29
+
30
+ export interface ExplainSuccess {
31
+ readonly status: 200;
32
+ readonly body: RoutingDecision;
33
+ }
34
+
35
+ export interface ExplainBadRequest {
36
+ readonly status: 400;
37
+ readonly body: ExplainValidationError;
38
+ }
39
+
40
+ export interface ExplainUnavailable {
41
+ readonly status: 503;
42
+ readonly body: RoutingDecision;
43
+ }
44
+
45
+ export type ExplainResult = ExplainSuccess | ExplainBadRequest | ExplainUnavailable;
46
+
47
+ // ─── Handler dependencies ─────────────────────────────────────────────────────
48
+
49
+ export interface ExplainHandlerDeps {
50
+ readonly fleet: readonly ModelProfile[];
51
+ readonly pipeline: RouterPipeline;
52
+ readonly clusterMatcher?: ClusterMatcher;
53
+ }
54
+
55
+ // ─── Handler factory ──────────────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Create an explain handler bound to a shared pipeline instance.
59
+ *
60
+ * Reuses the same RouterPipeline as the live dispatch path so
61
+ * session pin state and stage ordering are identical.
62
+ */
63
+ export function createExplainHandler(deps: ExplainHandlerDeps) {
64
+ const { fleet, pipeline, clusterMatcher } = deps;
65
+
66
+ return async function explain(rawBody: unknown): Promise<ExplainResult> {
67
+ const parsed = RoutingRequestSchema.safeParse(rawBody);
68
+
69
+ if (!parsed.success) {
70
+ return {
71
+ status: 400,
72
+ body: {
73
+ error: 'validation_failed',
74
+ details: parsed.error.issues.map(
75
+ (issue) => `${issue.path.join('.')}: ${issue.message}`,
76
+ ),
77
+ },
78
+ };
79
+ }
80
+
81
+ const request = toRoutingRequest(parsed.data);
82
+ const start = performance.now();
83
+
84
+ try {
85
+ const decision = await pipeline.route(request);
86
+ return {
87
+ status: 200,
88
+ body: await enrichRoutingDecisionForExplain(request, decision, {
89
+ fleet,
90
+ ...(clusterMatcher !== undefined ? { clusterMatcher } : {}),
91
+ }),
92
+ };
93
+ } catch {
94
+ const fallbackModel = safeCloudDefault(fleet);
95
+ const decision: RoutingDecision = {
96
+ request_id: request.request_id,
97
+ selected_model_id: fallbackModel?.id ?? 'unknown',
98
+ tier: fallbackModel?.tier ?? 'economical-cloud',
99
+ stage: 'fallback',
100
+ reason_code: 'safe_default',
101
+ routing_latency_ms: performance.now() - start,
102
+ pin_reason: null,
103
+ };
104
+ return {
105
+ status: 503,
106
+ body: await enrichRoutingDecisionForExplain(request, decision, {
107
+ fleet,
108
+ ...(clusterMatcher !== undefined ? { clusterMatcher } : {}),
109
+ }),
110
+ };
111
+ }
112
+ };
113
+ }
114
+
115
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
116
+
117
+ type ParsedRequest = ReturnType<typeof RoutingRequestSchema.parse>;
118
+
119
+ /**
120
+ * Map Zod-validated data to the domain RoutingRequest entity, bridging
121
+ * schema field names (tool_call_id/tool_calls) to entity shape (tool_blocks)
122
+ * and satisfying exactOptionalPropertyTypes constraints.
123
+ */
124
+ function toRoutingRequest(data: ParsedRequest): RoutingRequest {
125
+ // Build with known required fields, then add optional fields only when present
126
+ // to satisfy exactOptionalPropertyTypes (no explicit undefined values).
127
+ const result: { request_id: string; session_id: string; prompt_text: string; [k: string]: unknown } = {
128
+ request_id: data.request_id,
129
+ session_id: data.session_id,
130
+ prompt_text: data.prompt_text,
131
+ };
132
+
133
+ if (data.messages !== undefined) {
134
+ result['messages'] = data.messages.map((m): Message => ({
135
+ role: m.role,
136
+ content: m.content,
137
+ ...(m.tool_calls !== undefined ? { tool_blocks: m.tool_calls } : {}),
138
+ }));
139
+ }
140
+ if (data.turn_type !== undefined) result['turn_type'] = data.turn_type;
141
+ if (data.compaction_flag !== undefined) result['compaction_flag'] = data.compaction_flag;
142
+ if (data.force_model_id !== undefined) result['force_model_id'] = data.force_model_id;
143
+ if (data.estimated_input_tokens !== undefined) result['estimated_input_tokens'] = data.estimated_input_tokens;
144
+
145
+ return result as RoutingRequest;
146
+ }
File without changes
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Pi lifecycle hook integration — T021, T021b, SP-055.
3
+ *
4
+ * Registers compaction and model-override hooks on pi extension events.
5
+ * Lifecycle flags are consumed when building the next routing request
6
+ * (extension `buildRoutingRequest` or embedder `dispatch.dispatch`).
7
+ *
8
+ * Routing and stream delegation live in `.pi/extensions/smart-router/` for
9
+ * pi users, or in embedder code that calls `GatewayDispatch.dispatch()`.
10
+ * This module does not register routing or no-op context hooks.
11
+ *
12
+ * Contract: specs/001-build-smart-router/contracts/pi-middleware.md v1.0.0
13
+ */
14
+
15
+ // ─── Pi extension event types (structural typing) ────────────────────────────
16
+
17
+ export interface PiSessionManager {
18
+ getSessionFile(): string | undefined;
19
+ getSessionId(): string;
20
+ }
21
+
22
+ export interface PiExtensionContext {
23
+ readonly cwd: string;
24
+ readonly sessionManager: PiSessionManager;
25
+ }
26
+
27
+ export interface PiMessage {
28
+ readonly role: string;
29
+ readonly content: string;
30
+ readonly tool_call_id?: string;
31
+ readonly tool_calls?: readonly unknown[];
32
+ }
33
+
34
+ export interface PiContextEvent {
35
+ readonly messages: readonly PiMessage[];
36
+ }
37
+
38
+ export interface PiModelSelectEvent {
39
+ readonly source: string;
40
+ readonly model: {
41
+ readonly provider: string;
42
+ readonly id: string;
43
+ };
44
+ }
45
+
46
+ /** Legacy embedder event shape; routing is handled in the pi extension stream path. */
47
+ export interface PiProviderRequestEvent {
48
+ readonly provider: string;
49
+ readonly model: string;
50
+ readonly messages?: readonly PiMessage[];
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ export interface PiExtensionHooks {
55
+ on(event: 'before_provider_request', handler: (event: PiProviderRequestEvent, ctx: PiExtensionContext) => PiProviderRequestEvent | Promise<PiProviderRequestEvent>): void;
56
+ on(event: 'context', handler: (event: PiContextEvent, ctx: PiExtensionContext) => void): void;
57
+ on(event: 'session_compact', handler: (event: unknown, ctx: PiExtensionContext) => void): void;
58
+ on(event: 'session_before_compact', handler: (event: unknown, ctx: PiExtensionContext) => void): void;
59
+ on(event: 'model_select', handler: (event: PiModelSelectEvent, ctx: PiExtensionContext) => void): void;
60
+ }
61
+
62
+ // ─── Lifecycle hook state (FR-008) ───────────────────────────────────────────
63
+
64
+ export interface LifecycleFlags {
65
+ readonly compaction_flag?: boolean;
66
+ readonly force_model_id?: string;
67
+ }
68
+
69
+ interface SessionLifecycleState {
70
+ compactionPending: boolean;
71
+ forceModelId?: string;
72
+ }
73
+
74
+ /**
75
+ * Per-session lifecycle flags set by pi compaction and model_select hooks.
76
+ * Shared across router rebuilds so hook state survives fleet refresh.
77
+ */
78
+ export class LifecycleHookState {
79
+ private readonly sessions = new Map<string, SessionLifecycleState>();
80
+
81
+ private getOrCreate(sessionId: string): SessionLifecycleState {
82
+ let state = this.sessions.get(sessionId);
83
+ if (!state) {
84
+ state = { compactionPending: false };
85
+ this.sessions.set(sessionId, state);
86
+ }
87
+ return state;
88
+ }
89
+
90
+ markCompaction(sessionId: string): void {
91
+ this.getOrCreate(sessionId).compactionPending = true;
92
+ }
93
+
94
+ setForceModel(sessionId: string, modelId: string): void {
95
+ const state = this.getOrCreate(sessionId);
96
+ state.forceModelId = modelId;
97
+ }
98
+
99
+ /**
100
+ * Consume lifecycle flags for the next routing request.
101
+ * Compaction is one-shot; force_model_id is one-shot per consume.
102
+ */
103
+ consume(sessionId: string): LifecycleFlags {
104
+ const state = this.sessions.get(sessionId);
105
+ if (!state) {
106
+ return {};
107
+ }
108
+
109
+ const flags: LifecycleFlags = {
110
+ ...(state.compactionPending ? { compaction_flag: true } : {}),
111
+ ...(state.forceModelId !== undefined ? { force_model_id: state.forceModelId } : {}),
112
+ };
113
+
114
+ state.compactionPending = false;
115
+ delete state.forceModelId;
116
+
117
+ if (!state.compactionPending && state.forceModelId === undefined) {
118
+ this.sessions.delete(sessionId);
119
+ }
120
+
121
+ return flags;
122
+ }
123
+ }
124
+
125
+ function resolveHookSessionId(ctx: PiExtensionContext): string {
126
+ return ctx.sessionManager.getSessionId();
127
+ }
128
+
129
+ // ─── Lifecycle hook registrar ────────────────────────────────────────────────
130
+
131
+ export interface PiRouterMiddlewareOptions {
132
+ readonly lifecycleHookState?: LifecycleHookState;
133
+ }
134
+
135
+ export interface PiRouterMiddleware {
136
+ readonly register: (hooks: PiExtensionHooks) => void;
137
+ readonly lifecycleHookState: LifecycleHookState;
138
+ }
139
+
140
+ /**
141
+ * Create pi lifecycle hook handlers for session compaction and model overrides.
142
+ *
143
+ * Library embedders: call `router.register(hooks)` on the returned `RouterHandle`
144
+ * and route via `router.dispatch.dispatch()`. For pi, use the project extension at
145
+ * `.pi/extensions/smart-router/` — it owns stream delegation and routing telemetry.
146
+ */
147
+ export function createPiRouterMiddleware(
148
+ options?: PiRouterMiddlewareOptions,
149
+ ): PiRouterMiddleware {
150
+ const lifecycleHookState = options?.lifecycleHookState ?? new LifecycleHookState();
151
+
152
+ function register(hooks: PiExtensionHooks): void {
153
+ hooks.on('session_compact', (_event: unknown, ctx: PiExtensionContext) => {
154
+ lifecycleHookState.markCompaction(resolveHookSessionId(ctx));
155
+ });
156
+
157
+ hooks.on('session_before_compact', (_event: unknown, ctx: PiExtensionContext) => {
158
+ lifecycleHookState.markCompaction(resolveHookSessionId(ctx));
159
+ });
160
+
161
+ hooks.on('model_select', (event: PiModelSelectEvent, ctx: PiExtensionContext) => {
162
+ if (event.source === 'set') {
163
+ lifecycleHookState.setForceModel(resolveHookSessionId(ctx), event.model.id);
164
+ }
165
+ });
166
+ }
167
+
168
+ return { register, lifecycleHookState };
169
+ }