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,562 @@
1
+ import type {
2
+ Api,
3
+ AssistantMessage,
4
+ AssistantMessageEventStream,
5
+ Context,
6
+ Model,
7
+ SimpleStreamOptions,
8
+ } from '@earendil-works/pi-ai/compat';
9
+
10
+ import { safeCloudDefault } from '../../../src/domain/pipeline/safe-default.js';
11
+ import { computeOutputHeadroom } from '../../../src/domain/delegation/output-headroom.js';
12
+ import {
13
+ CONTEXT_OVERFLOW_NO_FIT,
14
+ resolveContextOverflowFallback,
15
+ } from '../../../src/domain/routing/context-fit.js';
16
+ import {
17
+ assertRoutableFleetAfterGeminiToolHistoryGuard,
18
+ GEMINI_TOOL_HISTORY_EXCLUDED,
19
+ resolveEffectiveFleet,
20
+ } from '../../../src/domain/routing/tool-history-guard.js';
21
+ import type { ModelProfile, RoutingDecision, RoutingRequest } from '../../../src/domain/types/index.js';
22
+ import type { GeminiToolHistoryGuardResult } from '../../../src/domain/routing/tool-history-guard.js';
23
+ import {
24
+ isGeminiThoughtSignatureAssistantError,
25
+ parseAssistantMessageError,
26
+ } from '../../../src/infrastructure/delegation/provider-error.js';
27
+ import { shouldFailoverOnProviderError } from '../../../src/infrastructure/gateway/gateway-dispatch.js';
28
+ import { delegateWithOutcome } from './delegate-stream.js';
29
+ import {
30
+ findFleetProfile,
31
+ flushDelegatedEvents,
32
+ injectFailoverNotice,
33
+ resolveRegistryModel,
34
+ } from './delegation-runtime.js';
35
+ import { buildRoutingRequest } from './routing-context.js';
36
+ import { capturePreRouteOutcomes, updateSessionRoutingSnapshot } from './routing-outcomes.js';
37
+ import type { StreamDelegationDeps } from './types.js';
38
+
39
+ function isRoutingLogEnabled(): boolean {
40
+ return process.env.SMART_ROUTER_LOG_ROUTING === '1';
41
+ }
42
+
43
+ function resolveFailoverProviderError(
44
+ message: AssistantMessage,
45
+ ): ReturnType<typeof parseAssistantMessageError> {
46
+ const parsed = parseAssistantMessageError(message);
47
+ if (parsed) {
48
+ return parsed;
49
+ }
50
+ if (message.stopReason === 'error' && message.errorMessage) {
51
+ return { message: message.errorMessage };
52
+ }
53
+ return undefined;
54
+ }
55
+
56
+ function logRoutingDecision(
57
+ decision: RoutingDecision,
58
+ delegate?: { provider: string; modelId: string; api: Api },
59
+ ): void {
60
+ if (!isRoutingLogEnabled()) {
61
+ return;
62
+ }
63
+
64
+ console.warn(
65
+ '[smart-router] routing decision',
66
+ JSON.stringify({
67
+ request_id: decision.request_id,
68
+ selected_model_id: decision.selected_model_id,
69
+ tier: decision.tier,
70
+ stage: decision.stage,
71
+ reason_code: decision.reason_code,
72
+ routing_latency_ms: decision.routing_latency_ms,
73
+ features: decision.features ?? null,
74
+ delegate,
75
+ }),
76
+ );
77
+ }
78
+
79
+ export function resolveTargetModel(
80
+ deps: StreamDelegationDeps,
81
+ decision: RoutingDecision,
82
+ ): Model<Api> | undefined {
83
+ const profile = findFleetProfile(deps.fleet, decision.selected_model_id);
84
+ if (!profile) {
85
+ return undefined;
86
+ }
87
+ return resolveRegistryModel(deps.modelRegistry, profile);
88
+ }
89
+
90
+ function resolveFallbackModel(
91
+ deps: StreamDelegationDeps,
92
+ effectiveFleet?: readonly ModelProfile[],
93
+ ): Model<Api> | undefined {
94
+ const fallbackProfile = safeCloudDefault(effectiveFleet ?? deps.fleet);
95
+ if (!fallbackProfile) {
96
+ return undefined;
97
+ }
98
+ return resolveRegistryModel(deps.modelRegistry, fallbackProfile);
99
+ }
100
+
101
+ function createContextOverflowErrorMessage(
102
+ model: Model<Api>,
103
+ reasonCode: string,
104
+ ): AssistantMessage {
105
+ return {
106
+ role: 'assistant',
107
+ content: [],
108
+ api: model.api,
109
+ provider: model.provider,
110
+ model: model.id,
111
+ usage: {
112
+ input: 0,
113
+ output: 0,
114
+ cacheRead: 0,
115
+ cacheWrite: 0,
116
+ totalTokens: 0,
117
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
118
+ },
119
+ stopReason: 'error',
120
+ errorMessage: `Context overflow: no model can fit input with required output headroom (${reasonCode})`,
121
+ timestamp: Date.now(),
122
+ };
123
+ }
124
+
125
+ function emitContextOverflowNoFit(
126
+ outer: AssistantMessageEventStream,
127
+ model: Model<Api>,
128
+ reasonCode: string,
129
+ ): void {
130
+ const errorMessage = createContextOverflowErrorMessage(model, reasonCode);
131
+ outer.push({ type: 'error', reason: 'error', error: errorMessage });
132
+ outer.end(errorMessage);
133
+ }
134
+
135
+ function isZeroOutputLengthStop(message: AssistantMessage): boolean {
136
+ return message.stopReason === 'length' && message.usage.output === 0;
137
+ }
138
+
139
+ function buildOverflowRoutingDecision(
140
+ base: RoutingDecision,
141
+ fallback: ReturnType<typeof resolveContextOverflowFallback>,
142
+ ): RoutingDecision {
143
+ if (fallback.kind === 'no_fit' || !fallback.model) {
144
+ return {
145
+ ...base,
146
+ selected_model_id: 'unknown',
147
+ reason_code: fallback.reasonCode,
148
+ };
149
+ }
150
+
151
+ return {
152
+ ...base,
153
+ selected_model_id: fallback.model.id,
154
+ tier: fallback.model.tier,
155
+ reason_code: fallback.reasonCode,
156
+ };
157
+ }
158
+
159
+ function resolveHeadroomFallbackTarget(
160
+ deps: StreamDelegationDeps,
161
+ request: RoutingRequest,
162
+ effectiveFleet: readonly ModelProfile[],
163
+ targetModel: Model<Api>,
164
+ targetProfile: ModelProfile,
165
+ estimatedInputTokens: number,
166
+ excludeModelIds: readonly string[],
167
+ ):
168
+ | { kind: 'fit'; model: Model<Api>; profile: ModelProfile; decision: RoutingDecision }
169
+ | { kind: 'no_fit'; reasonCode: typeof CONTEXT_OVERFLOW_NO_FIT } {
170
+ const overflow = resolveContextOverflowFallback(
171
+ effectiveFleet,
172
+ request,
173
+ targetProfile.provider,
174
+ );
175
+
176
+ if (overflow.kind === 'no_fit' || !overflow.model) {
177
+ return { kind: 'no_fit', reasonCode: CONTEXT_OVERFLOW_NO_FIT };
178
+ }
179
+
180
+ if (excludeModelIds.includes(overflow.model.id)) {
181
+ return { kind: 'no_fit', reasonCode: CONTEXT_OVERFLOW_NO_FIT };
182
+ }
183
+
184
+ const alternateModel = resolveRegistryModel(deps.modelRegistry, overflow.model);
185
+ if (!alternateModel) {
186
+ return { kind: 'no_fit', reasonCode: CONTEXT_OVERFLOW_NO_FIT };
187
+ }
188
+
189
+ const headroom = computeOutputHeadroom(
190
+ overflow.model,
191
+ estimatedInputTokens,
192
+ undefined,
193
+ alternateModel,
194
+ );
195
+ if (headroom.kind === 'no_fit') {
196
+ return { kind: 'no_fit', reasonCode: CONTEXT_OVERFLOW_NO_FIT };
197
+ }
198
+
199
+ return {
200
+ kind: 'fit',
201
+ model: alternateModel,
202
+ profile: overflow.model,
203
+ decision: buildOverflowRoutingDecision(
204
+ {
205
+ request_id: request.request_id,
206
+ selected_model_id: overflow.model.id,
207
+ tier: overflow.model.tier,
208
+ stage: 'fallback',
209
+ reason_code: overflow.reasonCode,
210
+ routing_latency_ms: 0,
211
+ pin_reason: 'context_overflow',
212
+ },
213
+ overflow,
214
+ ),
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Route a request and delegate to the selected provider with failover.
220
+ * Kept as one module to preserve the atomic failover state machine (#33).
221
+ */
222
+ export async function routeAndDelegate(
223
+ context: Context,
224
+ options: SimpleStreamOptions | undefined,
225
+ deps: StreamDelegationDeps,
226
+ outer: AssistantMessageEventStream,
227
+ ): Promise<void> {
228
+ const sessionId = options?.sessionId;
229
+ if (deps.ensureFleetFresh) {
230
+ await deps.ensureFleetFresh();
231
+ }
232
+ let decision: RoutingDecision;
233
+ let request: RoutingRequest;
234
+ let effectiveFleet: readonly ModelProfile[] = deps.fleet;
235
+ let guardResult: GeminiToolHistoryGuardResult | undefined;
236
+ const priorSnapshot =
237
+ sessionId !== undefined ? deps.sessionRouting?.get(sessionId) : undefined;
238
+ const hadPin =
239
+ sessionId !== undefined && deps.sessionPinner
240
+ ? deps.sessionPinner.getPin(sessionId) !== null
241
+ : false;
242
+
243
+ try {
244
+ request = buildRoutingRequest(
245
+ context,
246
+ options,
247
+ deps.lifecycleHookState,
248
+ );
249
+ guardResult = resolveEffectiveFleet(deps.fleet, request, context.messages);
250
+ effectiveFleet = guardResult.effectiveFleet;
251
+ assertRoutableFleetAfterGeminiToolHistoryGuard(guardResult);
252
+ if (guardResult.excluded) {
253
+ console.warn(
254
+ '[smart-router] gemini tool history guard applied',
255
+ JSON.stringify({
256
+ reason_code: GEMINI_TOOL_HISTORY_EXCLUDED,
257
+ session_id: request.session_id,
258
+ excluded_providers: ['google', 'gemini'],
259
+ }),
260
+ );
261
+ }
262
+ capturePreRouteOutcomes(request, deps, priorSnapshot, hadPin);
263
+ decision = await deps.router.dispatch.dispatch(request, { effectiveFleet });
264
+ } catch (error) {
265
+ const fallbackModel = resolveFallbackModel(deps, effectiveFleet);
266
+ if (!fallbackModel) {
267
+ throw error;
268
+ }
269
+ console.warn(
270
+ '[smart-router] routing failed, using safe cloud default',
271
+ error instanceof Error ? error.message : String(error),
272
+ );
273
+ const fallbackResult = await delegateWithOutcome(
274
+ fallbackModel,
275
+ context,
276
+ deps,
277
+ options,
278
+ sessionId,
279
+ );
280
+ flushDelegatedEvents(outer, fallbackResult.events, {
281
+ contextWindow: fallbackModel.contextWindow,
282
+ });
283
+ return;
284
+ }
285
+
286
+ deps.onRoutingDecision?.(decision);
287
+ deps.datasetRecorder?.record(request, decision);
288
+ updateSessionRoutingSnapshot(deps, sessionId, request, decision);
289
+
290
+ let targetModel = resolveTargetModel(deps, decision);
291
+ if (!targetModel) {
292
+ console.warn(
293
+ '[smart-router] routed model not found in registry',
294
+ decision.selected_model_id,
295
+ );
296
+ targetModel = resolveFallbackModel(deps, effectiveFleet);
297
+ }
298
+
299
+ if (!targetModel) {
300
+ if (decision.selected_model_id === 'unknown' && guardResult) {
301
+ assertRoutableFleetAfterGeminiToolHistoryGuard(guardResult);
302
+ }
303
+ throw new Error(
304
+ `No registry model available for routing decision ${decision.selected_model_id}`,
305
+ );
306
+ }
307
+
308
+ logRoutingDecision(decision, {
309
+ provider: targetModel.provider,
310
+ modelId: targetModel.id,
311
+ api: targetModel.api,
312
+ });
313
+
314
+ const failedModelIds: string[] = [];
315
+ const headroomExcludedModelIds: string[] = [];
316
+ const estimatedInputTokens =
317
+ request.estimated_input_tokens ?? request.prompt_text.length;
318
+ let pendingFailoverInfo: {
319
+ failedModelId: string;
320
+ alternateModelId: string;
321
+ errorObj?: ReturnType<typeof parseAssistantMessageError>;
322
+ } | undefined;
323
+
324
+ while (true) {
325
+ try {
326
+ const targetProfile =
327
+ findFleetProfile(effectiveFleet, targetModel.id) ??
328
+ findFleetProfile(deps.fleet, targetModel.id);
329
+
330
+ if (targetProfile) {
331
+ const headroom = computeOutputHeadroom(
332
+ targetProfile,
333
+ estimatedInputTokens,
334
+ undefined,
335
+ targetModel,
336
+ );
337
+ if (headroom.kind === 'no_fit') {
338
+ if (!headroomExcludedModelIds.includes(targetModel.id)) {
339
+ headroomExcludedModelIds.push(targetModel.id);
340
+ }
341
+ const fallbackTarget = resolveHeadroomFallbackTarget(
342
+ deps,
343
+ request,
344
+ effectiveFleet,
345
+ targetModel,
346
+ targetProfile,
347
+ estimatedInputTokens,
348
+ headroomExcludedModelIds,
349
+ );
350
+ if (fallbackTarget.kind === 'no_fit') {
351
+ emitContextOverflowNoFit(outer, targetModel, fallbackTarget.reasonCode);
352
+ return;
353
+ }
354
+
355
+ console.warn(
356
+ '[smart-router] output headroom exceeded, escalating to larger model',
357
+ fallbackTarget.model.id,
358
+ );
359
+ pendingFailoverInfo = {
360
+ failedModelId: targetModel.id,
361
+ alternateModelId: fallbackTarget.model.id,
362
+ errorObj: { message: 'insufficient output headroom' },
363
+ };
364
+ decision = fallbackTarget.decision;
365
+ deps.onRoutingDecision?.(decision);
366
+ targetModel = fallbackTarget.model;
367
+ continue;
368
+ }
369
+ }
370
+
371
+ const headroomContext = targetProfile
372
+ ? { profile: targetProfile, estimatedInputTokens }
373
+ : undefined;
374
+
375
+ const result = await delegateWithOutcome(
376
+ targetModel,
377
+ context,
378
+ deps,
379
+ options,
380
+ sessionId,
381
+ headroomContext,
382
+ );
383
+
384
+ if (pendingFailoverInfo) {
385
+ injectFailoverNotice(
386
+ result.events,
387
+ pendingFailoverInfo.failedModelId,
388
+ pendingFailoverInfo.alternateModelId,
389
+ pendingFailoverInfo.errorObj,
390
+ );
391
+ pendingFailoverInfo = undefined;
392
+ }
393
+
394
+ if (result.finalMessage && isZeroOutputLengthStop(result.finalMessage)) {
395
+ const lengthStopProfile =
396
+ findFleetProfile(effectiveFleet, targetModel.id) ??
397
+ findFleetProfile(deps.fleet, targetModel.id);
398
+ if (lengthStopProfile) {
399
+ if (!headroomExcludedModelIds.includes(targetModel.id)) {
400
+ headroomExcludedModelIds.push(targetModel.id);
401
+ }
402
+ const fallbackTarget = resolveHeadroomFallbackTarget(
403
+ deps,
404
+ request,
405
+ effectiveFleet,
406
+ targetModel,
407
+ lengthStopProfile,
408
+ estimatedInputTokens,
409
+ headroomExcludedModelIds,
410
+ );
411
+ if (fallbackTarget.kind === 'fit') {
412
+ console.warn(
413
+ '[smart-router] zero-output length stop, escalating to larger model',
414
+ fallbackTarget.model.id,
415
+ );
416
+ pendingFailoverInfo = {
417
+ failedModelId: targetModel.id,
418
+ alternateModelId: fallbackTarget.model.id,
419
+ errorObj: { message: 'zero-output length stop' },
420
+ };
421
+ decision = fallbackTarget.decision;
422
+ deps.onRoutingDecision?.(decision);
423
+ targetModel = fallbackTarget.model;
424
+ continue;
425
+ }
426
+ emitContextOverflowNoFit(outer, targetModel, fallbackTarget.reasonCode);
427
+ return;
428
+ }
429
+ }
430
+
431
+ if (
432
+ result.failed &&
433
+ result.finalMessage &&
434
+ isGeminiThoughtSignatureAssistantError(result.finalMessage)
435
+ ) {
436
+ flushDelegatedEvents(outer, result.events, {
437
+ sanitizeErrors: true,
438
+ contextWindow: targetModel.contextWindow,
439
+ });
440
+ return;
441
+ }
442
+
443
+ if (result.failed && result.finalMessage) {
444
+ const providerError = resolveFailoverProviderError(result.finalMessage);
445
+ const failedProfile = findFleetProfile(effectiveFleet, targetModel.id);
446
+ if (
447
+ providerError &&
448
+ shouldFailoverOnProviderError(providerError, failedProfile)
449
+ ) {
450
+ deps.router.dispatch.recordOutcome(targetModel.id, providerError);
451
+ failedModelIds.push(targetModel.id);
452
+ const failover = deps.router.dispatch.selectFailover(
453
+ decision,
454
+ failedModelIds,
455
+ effectiveFleet,
456
+ );
457
+ if (!failover) {
458
+ flushDelegatedEvents(outer, result.events, {
459
+ sanitizeErrors: true,
460
+ contextWindow: targetModel.contextWindow,
461
+ });
462
+ return;
463
+ }
464
+
465
+ const alternateModel = resolveTargetModel(deps, failover);
466
+ if (!alternateModel || alternateModel.id === targetModel.id) {
467
+ flushDelegatedEvents(outer, result.events, {
468
+ sanitizeErrors: true,
469
+ contextWindow: targetModel.contextWindow,
470
+ });
471
+ return;
472
+ }
473
+
474
+ console.warn(
475
+ '[smart-router] infra error, failing over to alternate model',
476
+ alternateModel.id,
477
+ );
478
+ pendingFailoverInfo = {
479
+ failedModelId: targetModel.id,
480
+ alternateModelId: alternateModel.id,
481
+ errorObj: providerError,
482
+ };
483
+ decision = failover;
484
+ targetModel = alternateModel;
485
+ continue;
486
+ }
487
+ }
488
+
489
+ flushDelegatedEvents(outer, result.events, {
490
+ sanitizeErrors: result.failed,
491
+ contextWindow: targetModel.contextWindow,
492
+ });
493
+ return;
494
+ } catch (error) {
495
+ deps.router.dispatch.recordOutcome(targetModel.id, { code: 'STREAM_DELEGATION_ERROR' });
496
+
497
+ if (!failedModelIds.includes(targetModel.id)) {
498
+ failedModelIds.push(targetModel.id);
499
+ }
500
+
501
+ const failover = deps.router.dispatch.selectFailover(
502
+ decision,
503
+ failedModelIds,
504
+ effectiveFleet,
505
+ );
506
+ const alternateModel = failover ? resolveTargetModel(deps, failover) : undefined;
507
+
508
+ if (alternateModel && alternateModel.id !== targetModel.id) {
509
+ console.warn(
510
+ '[smart-router] stream delegation failed, failing over',
511
+ error instanceof Error ? error.message : String(error),
512
+ );
513
+ pendingFailoverInfo = {
514
+ failedModelId: targetModel.id,
515
+ alternateModelId: alternateModel.id,
516
+ errorObj: { message: error instanceof Error ? error.message : String(error) },
517
+ };
518
+ if (!failover) {
519
+ throw error;
520
+ }
521
+ decision = failover;
522
+ targetModel = alternateModel;
523
+ continue;
524
+ }
525
+
526
+ const fallbackModel = resolveFallbackModel(deps, effectiveFleet);
527
+ if (!fallbackModel || fallbackModel.id === targetModel.id) {
528
+ throw error;
529
+ }
530
+
531
+ console.warn(
532
+ '[smart-router] stream delegation failed, using safe cloud default',
533
+ error instanceof Error ? error.message : String(error),
534
+ );
535
+ pendingFailoverInfo = {
536
+ failedModelId: targetModel.id,
537
+ alternateModelId: fallbackModel.id,
538
+ errorObj: { message: error instanceof Error ? error.message : String(error) },
539
+ };
540
+
541
+ const fallbackResult = await delegateWithOutcome(
542
+ fallbackModel,
543
+ context,
544
+ deps,
545
+ options,
546
+ sessionId,
547
+ );
548
+ if (pendingFailoverInfo) {
549
+ injectFailoverNotice(
550
+ fallbackResult.events,
551
+ pendingFailoverInfo.failedModelId,
552
+ pendingFailoverInfo.alternateModelId,
553
+ pendingFailoverInfo.errorObj,
554
+ );
555
+ }
556
+ flushDelegatedEvents(outer, fallbackResult.events, {
557
+ contextWindow: fallbackModel.contextWindow,
558
+ });
559
+ return;
560
+ }
561
+ }
562
+ }