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,355 @@
1
+ /**
2
+ * Delegation helpers: auth merge, event flush/sanitize, failover notice injection, registry lookups.
3
+ */
4
+
5
+ import {
6
+ type Api,
7
+ type AssistantMessage,
8
+ type AssistantMessageEvent,
9
+ type AssistantMessageEventStream,
10
+ type Context,
11
+ type Model,
12
+ type SimpleStreamOptions,
13
+ } from '@earendil-works/pi-ai/compat';
14
+ import type { ModelRegistry } from '@earendil-works/pi-coding-agent';
15
+
16
+ import { normalizeDelegationContext } from '../../../src/domain/delegation/delegation-context.js';
17
+ import {
18
+ computeOutputHeadroom,
19
+ type OutputHeadroomConfig,
20
+ } from '../../../src/domain/delegation/output-headroom.js';
21
+ import type { ModelProfile } from '../../../src/domain/types/index.js';
22
+ import {
23
+ formatGeminiThoughtSignatureErrorMessage,
24
+ formatProviderErrorMessage,
25
+ isGeminiThoughtSignatureAssistantError,
26
+ parseAssistantMessageError,
27
+ sanitizeLengthStopMessage,
28
+ type LengthStopHints,
29
+ } from '../../../src/infrastructure/delegation/provider-error.js';
30
+ import type { StreamDelegationDeps } from './types.js';
31
+
32
+ /** Stream options safe to forward to a delegated provider call. */
33
+ const DELEGATION_CALLER_OPTION_KEYS = [
34
+ 'signal',
35
+ 'sessionId',
36
+ 'reasoning',
37
+ 'thinkingBudgets',
38
+ 'temperature',
39
+ 'maxTokens',
40
+ 'transport',
41
+ 'cacheRetention',
42
+ 'timeoutMs',
43
+ 'maxRetries',
44
+ 'maxRetryDelayMs',
45
+ 'metadata',
46
+ 'websocketConnectTimeoutMs',
47
+ ] as const satisfies readonly (keyof SimpleStreamOptions)[];
48
+
49
+ function pickDelegationCallerOptions(
50
+ callerOptions?: SimpleStreamOptions,
51
+ ): SimpleStreamOptions {
52
+ if (!callerOptions) {
53
+ return {};
54
+ }
55
+
56
+ const picked: SimpleStreamOptions = {};
57
+ const source = callerOptions as SimpleStreamOptions & Record<string, unknown>;
58
+ for (const key of DELEGATION_CALLER_OPTION_KEYS) {
59
+ if (source[key] !== undefined) {
60
+ (picked as Record<string, unknown>)[key] = source[key];
61
+ }
62
+ }
63
+ return picked;
64
+ }
65
+
66
+ export class DelegationHeadroomError extends Error {
67
+ readonly reason = 'delegation_output_headroom_exceeded' as const;
68
+
69
+ constructor(message: string) {
70
+ super(message);
71
+ this.name = 'DelegationHeadroomError';
72
+ }
73
+ }
74
+
75
+ export interface DelegationHeadroomContext {
76
+ readonly profile: ModelProfile;
77
+ readonly estimatedInputTokens: number;
78
+ readonly headroomConfig?: OutputHeadroomConfig;
79
+ }
80
+
81
+ export async function resolveDelegationOptions(
82
+ modelRegistry: ModelRegistry,
83
+ targetModel: Model<Api>,
84
+ callerOptions?: SimpleStreamOptions,
85
+ headroomContext?: DelegationHeadroomContext,
86
+ ): Promise<SimpleStreamOptions> {
87
+ const auth = await modelRegistry.getApiKeyAndHeaders(targetModel);
88
+ if (!auth.ok) {
89
+ throw new Error(auth.error);
90
+ }
91
+
92
+ const callerEnv = callerOptions?.env;
93
+ const mergedEnv =
94
+ auth.env || callerEnv
95
+ ? { ...(auth.env ?? {}), ...(callerEnv ?? {}) }
96
+ : undefined;
97
+
98
+ let maxTokens: number | undefined;
99
+ if (headroomContext) {
100
+ const headroom = computeOutputHeadroom(
101
+ headroomContext.profile,
102
+ headroomContext.estimatedInputTokens,
103
+ headroomContext.headroomConfig,
104
+ targetModel,
105
+ );
106
+ if (headroom.kind === 'no_fit') {
107
+ throw new DelegationHeadroomError(
108
+ `Output headroom below floor for ${targetModel.id}: ` +
109
+ `${headroomContext.estimatedInputTokens} input tokens on ` +
110
+ `${headroom.contextWindow} context window ` +
111
+ `(${headroom.availableOutputTokens} output tokens available)`,
112
+ );
113
+ }
114
+ maxTokens = headroom.maxTokens;
115
+ }
116
+
117
+ return {
118
+ ...pickDelegationCallerOptions(callerOptions),
119
+ ...(auth.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
120
+ ...(auth.headers !== undefined ? { headers: auth.headers } : {}),
121
+ ...(mergedEnv !== undefined ? { env: mergedEnv } : {}),
122
+ ...(maxTokens !== undefined ? { maxTokens } : {}),
123
+ };
124
+ }
125
+
126
+ export interface DelegatedStreamResult {
127
+ readonly finalMessage: AssistantMessage | undefined;
128
+ readonly failed: boolean;
129
+ readonly events: AssistantMessageEvent[];
130
+ }
131
+
132
+ export function modelToExecutionModel(model: Model<Api>) {
133
+ return {
134
+ provider: model.provider,
135
+ api: model.api,
136
+ id: model.id,
137
+ };
138
+ }
139
+
140
+ export function buildDelegationContext(
141
+ context: Context,
142
+ targetModel: Model<Api>,
143
+ deps: StreamDelegationDeps,
144
+ sessionId: string | undefined,
145
+ ): Context {
146
+ const sessionExecution = sessionId
147
+ ? deps.executionLedger.getLastExecution(sessionId)
148
+ : null;
149
+
150
+ return normalizeDelegationContext(context, targetModel, {
151
+ sessionExecution,
152
+ });
153
+ }
154
+
155
+ export function createErrorMessage(
156
+ model: Model<Api>,
157
+ options: SimpleStreamOptions | undefined,
158
+ error: unknown,
159
+ ): AssistantMessage {
160
+ return {
161
+ role: 'assistant',
162
+ content: [],
163
+ api: model.api,
164
+ provider: model.provider,
165
+ model: model.id,
166
+ usage: {
167
+ input: 0,
168
+ output: 0,
169
+ cacheRead: 0,
170
+ cacheWrite: 0,
171
+ totalTokens: 0,
172
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
173
+ },
174
+ stopReason: options?.signal?.aborted ? 'aborted' : 'error',
175
+ errorMessage: formatProviderErrorMessage(
176
+ error instanceof Error ? error.message : String(error),
177
+ ),
178
+ timestamp: Date.now(),
179
+ };
180
+ }
181
+
182
+ function sanitizeAssistantErrorMessage(
183
+ message: AssistantMessage,
184
+ lengthStopHints?: LengthStopHints,
185
+ ): AssistantMessage {
186
+ const lengthSanitized = sanitizeLengthStopMessage(message, lengthStopHints);
187
+ if (lengthSanitized.stopReason !== 'error' || !lengthSanitized.errorMessage) {
188
+ return lengthSanitized;
189
+ }
190
+
191
+ const formatted = isGeminiThoughtSignatureAssistantError(lengthSanitized)
192
+ ? formatGeminiThoughtSignatureErrorMessage(lengthSanitized.errorMessage)
193
+ : formatProviderErrorMessage(lengthSanitized.errorMessage);
194
+ if (formatted === lengthSanitized.errorMessage) {
195
+ return lengthSanitized;
196
+ }
197
+ return { ...lengthSanitized, errorMessage: formatted };
198
+ }
199
+
200
+ function sanitizeDelegatedErrorEvents(
201
+ events: AssistantMessageEvent[],
202
+ lengthStopHints?: LengthStopHints,
203
+ ): void {
204
+ for (let i = 0; i < events.length; i++) {
205
+ const event = events[i];
206
+ if (!event) {
207
+ continue;
208
+ }
209
+ if (event.type === 'error') {
210
+ events[i] = {
211
+ ...event,
212
+ error: sanitizeAssistantErrorMessage(event.error, lengthStopHints),
213
+ };
214
+ } else if (event.type === 'done' && event.message.stopReason === 'error') {
215
+ events[i] = {
216
+ ...event,
217
+ message: sanitizeAssistantErrorMessage(event.message, lengthStopHints),
218
+ };
219
+ }
220
+ }
221
+ }
222
+
223
+ function sanitizeLengthStopEvents(
224
+ events: AssistantMessageEvent[],
225
+ lengthStopHints?: LengthStopHints,
226
+ ): void {
227
+ if (!lengthStopHints?.contextWindow) {
228
+ return;
229
+ }
230
+
231
+ for (let i = 0; i < events.length; i++) {
232
+ const event = events[i];
233
+ if (!event) {
234
+ continue;
235
+ }
236
+
237
+ if (event.type === 'done' && event.message.stopReason === 'length') {
238
+ const sanitized = sanitizeLengthStopMessage(event.message, lengthStopHints);
239
+ if (sanitized.stopReason === 'error') {
240
+ events[i] = {
241
+ type: 'error',
242
+ reason: 'error',
243
+ error: sanitized,
244
+ };
245
+ } else {
246
+ events[i] = { ...event, message: sanitized };
247
+ }
248
+ } else if (event.type === 'error' && event.error.stopReason === 'length') {
249
+ const sanitized = sanitizeLengthStopMessage(event.error, lengthStopHints);
250
+ if (sanitized.stopReason === 'error') {
251
+ events[i] = { ...event, error: sanitized, reason: 'error' };
252
+ } else {
253
+ events[i] = { ...event, error: sanitized };
254
+ }
255
+ }
256
+ }
257
+ }
258
+
259
+ export interface FlushDelegatedEventsOptions {
260
+ readonly sanitizeErrors?: boolean;
261
+ readonly contextWindow?: number;
262
+ }
263
+
264
+ export function flushDelegatedEvents(
265
+ outer: AssistantMessageEventStream,
266
+ events: readonly AssistantMessageEvent[],
267
+ options?: FlushDelegatedEventsOptions,
268
+ ): void {
269
+ const lengthStopHints =
270
+ options?.contextWindow !== undefined
271
+ ? { contextWindow: options.contextWindow }
272
+ : undefined;
273
+ const mutableEvents = [...events];
274
+ sanitizeLengthStopEvents(mutableEvents, lengthStopHints);
275
+ if (options?.sanitizeErrors) {
276
+ sanitizeDelegatedErrorEvents(mutableEvents, lengthStopHints);
277
+ }
278
+ for (const event of mutableEvents) {
279
+ outer.push(event);
280
+ }
281
+ outer.end();
282
+ }
283
+
284
+ export function injectFailoverNotice(
285
+ events: AssistantMessageEvent[],
286
+ failedModelId: string,
287
+ alternateModelId: string,
288
+ errorObj?: ReturnType<typeof parseAssistantMessageError>,
289
+ ): void {
290
+ const reason = errorObj?.message || errorObj?.code || 'Unavailable';
291
+ const notice = `> ⚠️ **pi-smart-router failover:** \`${failedModelId}\` failed (${reason}). Retrying with \`${alternateModelId}\`...`;
292
+
293
+ let noticeInjected = false;
294
+
295
+ for (let i = 0; i < events.length; i++) {
296
+ const event = events[i];
297
+ if (!event) {
298
+ continue;
299
+ }
300
+
301
+ if (event.type === 'start') {
302
+ const newPartial = { ...event.partial, content: [...event.partial.content] };
303
+ const firstBlock = newPartial.content[0];
304
+ if (firstBlock && firstBlock.type === 'text') {
305
+ newPartial.content[0] = { ...firstBlock, text: notice + '\n\n' + firstBlock.text };
306
+ } else {
307
+ newPartial.content.unshift({ type: 'text', text: notice + '\n\n' });
308
+ }
309
+ events[i] = { ...event, partial: newPartial };
310
+ }
311
+
312
+ if (!noticeInjected && event.type === 'text_delta') {
313
+ events[i] = { ...event, delta: notice + '\n\n' + event.delta };
314
+ noticeInjected = true;
315
+ }
316
+
317
+ if (event.type === 'done' && event.message) {
318
+ const newMsg = { ...event.message, content: [...event.message.content] };
319
+ const firstBlock = newMsg.content[0];
320
+ if (firstBlock && firstBlock.type === 'text') {
321
+ newMsg.content[0] = { ...firstBlock, text: notice + '\n\n' + firstBlock.text };
322
+ } else {
323
+ newMsg.content.unshift({ type: 'text', text: notice + '\n\n' });
324
+ }
325
+ events[i] = { ...event, message: newMsg };
326
+ }
327
+ }
328
+
329
+ if (!noticeInjected) {
330
+ const startIdx = events.findIndex((e) => e.type === 'start');
331
+ const startEvent = startIdx === -1 ? undefined : events[startIdx];
332
+ if (startEvent?.type === 'start') {
333
+ events.splice(startIdx + 1, 0, {
334
+ type: 'text_delta',
335
+ contentIndex: 0,
336
+ delta: notice + '\n\n',
337
+ partial: startEvent.partial,
338
+ });
339
+ }
340
+ }
341
+ }
342
+
343
+ export function findFleetProfile(
344
+ fleet: readonly ModelProfile[],
345
+ modelId: string,
346
+ ): ModelProfile | undefined {
347
+ return fleet.find((profile) => profile.id === modelId);
348
+ }
349
+
350
+ export function resolveRegistryModel(
351
+ modelRegistry: ModelRegistry,
352
+ profile: ModelProfile,
353
+ ): Model<Api> | undefined {
354
+ return modelRegistry.find(profile.provider, profile.id);
355
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ AuthStorage,
3
+ ModelRegistry,
4
+ type ExtensionAPI,
5
+ } from '@earendil-works/pi-coding-agent';
6
+
7
+ import { ExecutionLedger } from '../../../src/domain/delegation/execution-ledger.js';
8
+ import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
9
+ import type { SessionRoutingSnapshot } from '../../../src/infrastructure/telemetry/outcome-recorder.js';
10
+ import { createRouterFromFleet, LifecycleHookState } from '../../../src/index.js';
11
+
12
+ import { registerSmartRouterCommand } from './commands.js';
13
+ import {
14
+ createExtensionDatasetRecorder,
15
+ createExtensionOutcomeRecorder,
16
+ } from './dataset-export.js';
17
+ import { createDispatchOptions, initHydraMatcher } from './fleet-bootstrap.js';
18
+ import { setupSessionHooks } from './session-lifecycle.js';
19
+ import { createStreamSimple } from './stream-delegation.js';
20
+ import type { SmartRouterRuntime } from './types.js';
21
+ import { createExtensionStore } from './utils.js';
22
+
23
+ const PROVIDER_NAME = 'smart-router' as const;
24
+ const AUTO_MODEL_ID = 'auto' as const;
25
+
26
+ export type DatasetNotify = {
27
+ fn: ((message: string) => void) | undefined;
28
+ };
29
+
30
+ export async function createSmartRouterRuntime(cwd: string): Promise<{
31
+ runtime: SmartRouterRuntime;
32
+ datasetNotify: DatasetNotify;
33
+ }> {
34
+ const authStorage = AuthStorage.create();
35
+ const modelRegistry = ModelRegistry.inMemory(authStorage);
36
+ const hydraMatcher = await initHydraMatcher();
37
+ const store = createExtensionStore(cwd);
38
+ const sessionPinner = new SessionPinner({ store });
39
+ const executionLedger = new ExecutionLedger();
40
+ const lifecycleHookState = new LifecycleHookState();
41
+ const datasetNotify: DatasetNotify = {
42
+ fn: undefined,
43
+ };
44
+ const datasetRecorder = createExtensionDatasetRecorder(store, cwd, (message) => {
45
+ datasetNotify.fn?.(message);
46
+ });
47
+ const outcomeRecorder = createExtensionOutcomeRecorder(store);
48
+ const sessionRouting = new Map<string, SessionRoutingSnapshot>();
49
+
50
+ const runtime: SmartRouterRuntime = {
51
+ fleetMode: 'scoped',
52
+ lastDecision: undefined,
53
+ priceCatalog: null,
54
+ modelRegistry,
55
+ store,
56
+ sessionPinner,
57
+ executionLedger,
58
+ lifecycleHookState,
59
+ hydraMatcher,
60
+ datasetRecorder,
61
+ outcomeRecorder,
62
+ sessionRouting,
63
+ streamDeps: {
64
+ router: createRouterFromFleet([], {
65
+ ...createDispatchOptions(store, sessionPinner, hydraMatcher),
66
+ lifecycleHookState,
67
+ }),
68
+ modelRegistry,
69
+ fleet: [],
70
+ executionLedger,
71
+ lifecycleHookState,
72
+ datasetRecorder,
73
+ outcomeRecorder,
74
+ sessionPinner,
75
+ sessionRouting,
76
+ onRoutingDecision(decision) {
77
+ runtime.lastDecision = decision;
78
+ },
79
+ onDelegatedModel(model) {
80
+ runtime.setLmuStatus?.(model.id);
81
+ },
82
+ },
83
+ };
84
+
85
+ return { runtime, datasetNotify };
86
+ }
87
+
88
+ export async function wireSmartRouterExtension(
89
+ pi: ExtensionAPI,
90
+ runtime: SmartRouterRuntime,
91
+ datasetNotify: DatasetNotify,
92
+ ): Promise<void> {
93
+ registerSmartRouterCommand(pi, runtime);
94
+
95
+ setupSessionHooks(pi, runtime, runtime.sessionPinner, datasetNotify);
96
+
97
+ pi.registerProvider(PROVIDER_NAME, {
98
+ name: 'Smart Router',
99
+ baseUrl: 'https://smart-router.local',
100
+ apiKey: 'local',
101
+ api: 'openai-responses',
102
+ models: [
103
+ {
104
+ id: AUTO_MODEL_ID,
105
+ name: 'Auto (Smart Router)',
106
+ reasoning: true,
107
+ input: ['text', 'image'],
108
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
109
+ contextWindow: 200_000,
110
+ maxTokens: 16_384,
111
+ },
112
+ ],
113
+ streamSimple: createStreamSimple(runtime.streamDeps),
114
+ });
115
+ }
@@ -0,0 +1,244 @@
1
+ import type { Api, Model } from '@earendil-works/pi-ai/compat';
2
+ import {
3
+ ModelRegistry,
4
+ SettingsManager,
5
+ type ExtensionAPI,
6
+ } from '@earendil-works/pi-coding-agent';
7
+
8
+ import { mapFleetFromRegistry } from '../../../src/config/pi-model-mapper.js';
9
+ import { DEFAULT_OPERATOR_CONFIG } from '../../../src/config/defaults.js';
10
+ import {
11
+ HydraMatcher,
12
+ createOnnxEmbeddingProvider,
13
+ } from '../../../src/domain/matching/hydra-matcher.js';
14
+ import { SessionPinner } from '../../../src/domain/pinning/session-pinner.js';
15
+ import type { ModelProfile, PriceCatalog } from '../../../src/domain/types/index.js';
16
+ import type { StorePort } from '../../../src/domain/types/store-port.js';
17
+ import { getDefaultSystemInfo } from '../../../src/infrastructure/hardware/hardware-probe.js';
18
+ import { DEFAULT_LOCAL_CONFIG } from '../../../src/infrastructure/local/local-zero-tier.js';
19
+ import { RoutingTelemetryEmitter } from '../../../src/infrastructure/telemetry/routing-telemetry.js';
20
+ import { applyCatalogPricesToFleet } from '../../../src/infrastructure/pricing/price-broker.js';
21
+ import {
22
+ createRouterFromFleet,
23
+ type GatewayDispatchOptions,
24
+ type PiExtensionHooks,
25
+ } from '../../../src/index.js';
26
+ import { resolveModelScope } from './pi-model-scope.js';
27
+ import type { FleetMode, SmartRouterRuntime } from './types.js';
28
+ import { resolveRateLimiter } from './utils.js';
29
+
30
+ /** Minimal settings surface used for scoped fleet discovery. */
31
+ export interface ScopedSettingsReader {
32
+ getEnabledModels(): string[] | null | undefined;
33
+ }
34
+
35
+ export interface DiscoverFleetDeps {
36
+ settingsFactory?: (cwd: string) => ScopedSettingsReader;
37
+ }
38
+
39
+ /** Footer label for the last model that successfully served a delegated stream. */
40
+ export function formatLmuStatus(
41
+ modelId: string,
42
+ theme?: { fg: (color: string, text: string) => string },
43
+ ): string {
44
+ const label = `LMU: ${modelId}`;
45
+ return theme ? theme.fg('dim', label) : label;
46
+ }
47
+
48
+ export function createHooksAdapter(pi: ExtensionAPI): PiExtensionHooks {
49
+ return {
50
+ on(event, handler) {
51
+ // PiExtensionHooks event names are a subset of ExtensionAPI; cast bridges the gap.
52
+ pi.on(event as never, handler as never);
53
+ },
54
+ };
55
+ }
56
+
57
+ function registryModelsToFleetInput(models: readonly Model<Api>[]) {
58
+ return models.map((model) => ({
59
+ provider: model.provider,
60
+ id: model.id,
61
+ ...(model.name !== undefined ? { name: model.name } : {}),
62
+ cost: {
63
+ input: model.cost.input,
64
+ output: model.cost.output,
65
+ cacheRead: model.cost.cacheRead,
66
+ cacheWrite: model.cost.cacheWrite,
67
+ },
68
+ }));
69
+ }
70
+
71
+ /** Bind pi's shared model registry (includes package-registered providers). */
72
+ export function bindSharedModelRegistry(
73
+ runtime: SmartRouterRuntime,
74
+ modelRegistry: ModelRegistry,
75
+ ): void {
76
+ runtime.modelRegistry = modelRegistry;
77
+ runtime.streamDeps.modelRegistry = modelRegistry;
78
+ }
79
+
80
+ export function computeFleetScopeFingerprint(
81
+ mode: FleetMode,
82
+ patterns: readonly string[] | null | undefined,
83
+ available: readonly Model<Api>[],
84
+ catalogLastUpdated: string | null | undefined,
85
+ ): string {
86
+ const patternPart = patterns?.join('\n') ?? '';
87
+ const modelPart = available
88
+ .map((model) => `${model.provider}/${model.id}`)
89
+ .sort()
90
+ .join('\n');
91
+ return `${mode}\0${patternPart}\0${modelPart}\0${catalogLastUpdated ?? ''}`;
92
+ }
93
+
94
+ async function readScopeFingerprintInputs(
95
+ runtime: SmartRouterRuntime,
96
+ cwd: string,
97
+ deps?: DiscoverFleetDeps,
98
+ ): Promise<{ patterns: string[] | null | undefined; available: Model<Api>[] }> {
99
+ const available = await Promise.resolve(runtime.modelRegistry.getAvailable());
100
+ if (runtime.fleetMode !== 'scoped') {
101
+ return { patterns: null, available };
102
+ }
103
+
104
+ const settingsFactory = deps?.settingsFactory ?? SettingsManager.create;
105
+ const settings = settingsFactory(cwd);
106
+ return { patterns: settings.getEnabledModels(), available };
107
+ }
108
+
109
+ export async function computeCurrentFleetScopeFingerprint(
110
+ runtime: SmartRouterRuntime,
111
+ cwd: string,
112
+ deps?: DiscoverFleetDeps,
113
+ ): Promise<string> {
114
+ const { patterns, available } = await readScopeFingerprintInputs(runtime, cwd, deps);
115
+ const catalogLastUpdated = runtime.priceCatalog?.last_updated;
116
+ return computeFleetScopeFingerprint(
117
+ runtime.fleetMode,
118
+ patterns,
119
+ available,
120
+ catalogLastUpdated,
121
+ );
122
+ }
123
+
124
+ export async function discoverFleet(
125
+ modelRegistry: ModelRegistry,
126
+ mode: FleetMode,
127
+ cwd: string,
128
+ store: StorePort,
129
+ deps?: DiscoverFleetDeps,
130
+ ): Promise<{ fleet: ModelProfile[]; catalog: PriceCatalog | null }> {
131
+ const available = await Promise.resolve(modelRegistry.getAvailable());
132
+ let models = available;
133
+
134
+ if (mode === 'scoped') {
135
+ const settingsFactory = deps?.settingsFactory ?? SettingsManager.create;
136
+ const settings = settingsFactory(cwd);
137
+ const patterns = settings.getEnabledModels();
138
+ if (patterns && patterns.length > 0) {
139
+ const scopedModels = await resolveModelScope(patterns, modelRegistry);
140
+ models = scopedModels.map((scoped) => scoped.model);
141
+ }
142
+ }
143
+
144
+ const mappedFleet = mapFleetFromRegistry(registryModelsToFleetInput(models));
145
+ const catalog = await store.getPriceCatalog();
146
+ const fleet = applyCatalogPricesToFleet(mappedFleet, catalog);
147
+
148
+ return { fleet, catalog };
149
+ }
150
+
151
+ export function createDispatchOptions(
152
+ store: StorePort,
153
+ sessionPinner: SessionPinner,
154
+ hydraMatcher?: HydraMatcher,
155
+ ): GatewayDispatchOptions {
156
+ const telemetryEmitter = new RoutingTelemetryEmitter({
157
+ onRecord: (record) => {
158
+ store.appendTelemetry(record);
159
+ },
160
+ });
161
+ const rateLimiter = resolveRateLimiter(store);
162
+
163
+ return {
164
+ sessionPinner,
165
+ hardwareConfig: DEFAULT_OPERATOR_CONFIG.local,
166
+ systemInfoProvider: getDefaultSystemInfo,
167
+ localConfig: DEFAULT_LOCAL_CONFIG,
168
+ loopEscalationConfig: DEFAULT_OPERATOR_CONFIG.loop_escalation,
169
+ ...(hydraMatcher ? { hydraMatcher } : {}),
170
+ ...(rateLimiter ? { rateLimiter } : {}),
171
+ telemetryEmitter,
172
+ };
173
+ }
174
+
175
+ export async function rebuildFleet(
176
+ runtime: SmartRouterRuntime,
177
+ pi: ExtensionAPI,
178
+ cwd: string,
179
+ deps?: DiscoverFleetDeps,
180
+ ): Promise<void> {
181
+ const fingerprint = await computeCurrentFleetScopeFingerprint(runtime, cwd, deps);
182
+ const { fleet, catalog } = await discoverFleet(
183
+ runtime.modelRegistry,
184
+ runtime.fleetMode,
185
+ cwd,
186
+ runtime.store,
187
+ deps,
188
+ );
189
+ runtime.priceCatalog = catalog;
190
+ runtime.fleetScopeFingerprint = fingerprint;
191
+ const router = createRouterFromFleet(fleet, {
192
+ ...createDispatchOptions(runtime.store, runtime.sessionPinner, runtime.hydraMatcher),
193
+ lifecycleHookState: runtime.lifecycleHookState,
194
+ });
195
+ router.register(createHooksAdapter(pi));
196
+ runtime.streamDeps.router = router;
197
+ runtime.streamDeps.fleet = fleet;
198
+ }
199
+
200
+ /**
201
+ * Rebuild fleet only when scope fingerprint changed (mode, enabledModels, registry, pricing).
202
+ */
203
+ export async function ensureFleetFresh(
204
+ runtime: SmartRouterRuntime,
205
+ pi: ExtensionAPI,
206
+ cwd: string,
207
+ deps?: DiscoverFleetDeps,
208
+ ): Promise<void> {
209
+ const fingerprint = await computeCurrentFleetScopeFingerprint(runtime, cwd, deps);
210
+ if (fingerprint === runtime.fleetScopeFingerprint) {
211
+ return;
212
+ }
213
+ await rebuildFleet(runtime, pi, cwd, deps);
214
+ }
215
+
216
+ /**
217
+ * Optional HyDRA matcher bootstrap.
218
+ *
219
+ * Requires `@huggingface/transformers` at runtime (see root package.json).
220
+ * Install: `npm i @huggingface/transformers`
221
+ *
222
+ * pi exposes no extension teardown hook — ONNX provider dispose is a no-op.
223
+ */
224
+ export interface HydraInitDeps {
225
+ readonly createOnnxEmbeddingProvider?: typeof createOnnxEmbeddingProvider;
226
+ }
227
+
228
+ export async function initHydraMatcher(
229
+ deps?: HydraInitDeps,
230
+ ): Promise<HydraMatcher | undefined> {
231
+ const createProvider = deps?.createOnnxEmbeddingProvider ?? createOnnxEmbeddingProvider;
232
+ const artifactCachePath = DEFAULT_OPERATOR_CONFIG.hydra.artifact_cache_path;
233
+
234
+ try {
235
+ const provider = await createProvider(artifactCachePath);
236
+ return new HydraMatcher(provider, { artifactCachePath });
237
+ } catch (error) {
238
+ console.warn(
239
+ '[smart-router] HyDRA matcher disabled',
240
+ error instanceof Error ? error.message : String(error),
241
+ );
242
+ return undefined;
243
+ }
244
+ }