@iislee/opencodex 2.11.0 → 2.31.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 (675) hide show
  1. package/AGENTS_INSTALL.md +109 -0
  2. package/README.md +114 -19
  3. package/bin/ocx.mjs +164 -36
  4. package/bin/package-main.mjs +1 -1
  5. package/gui/dist/assets/index-BF38heuV.js +104 -0
  6. package/gui/dist/assets/index-DMiI18Kv.css +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  9. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  10. package/gui/dist/provider-icons/claude-color.svg +1 -1
  11. package/gui/dist/provider-icons/cline-color.svg +16 -0
  12. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  13. package/gui/dist/provider-icons/commandcode-color.svg +1 -0
  14. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  15. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  16. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  17. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  18. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  19. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  20. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  21. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  22. package/gui/dist/provider-icons/grok.svg +1 -1
  23. package/gui/dist/provider-icons/groq-color.svg +1 -1
  24. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  25. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  26. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  27. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  28. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  29. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  30. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  31. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  32. package/gui/dist/provider-icons/openai.svg +1 -1
  33. package/gui/dist/provider-icons/opencode.svg +2 -1
  34. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  35. package/gui/dist/provider-icons/pi.svg +2 -2
  36. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  37. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  38. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  39. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  40. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  41. package/package.json +19 -10
  42. package/src/adapters/anthropic-output-schema.ts +137 -0
  43. package/src/adapters/anthropic.ts +376 -52
  44. package/src/adapters/base.ts +54 -7
  45. package/src/adapters/client-fingerprint.ts +18 -12
  46. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +69 -0
  47. package/src/adapters/command-code.ts +601 -0
  48. package/src/adapters/cursor/checkpoint-store.ts +303 -0
  49. package/src/adapters/cursor/cursor-errors.ts +129 -6
  50. package/src/adapters/cursor/discovery.ts +61 -4
  51. package/src/adapters/cursor/effort-map.ts +27 -3
  52. package/src/adapters/cursor/framing.ts +39 -0
  53. package/src/adapters/cursor/h2-pool.ts +123 -0
  54. package/src/adapters/cursor/http1-bidi.ts +361 -0
  55. package/src/adapters/cursor/images.ts +704 -0
  56. package/src/adapters/cursor/live-models.ts +174 -58
  57. package/src/adapters/cursor/live-transport.ts +609 -170
  58. package/src/adapters/cursor/native-exec-common.ts +23 -2
  59. package/src/adapters/cursor/native-exec-fs.ts +10 -7
  60. package/src/adapters/cursor/native-exec-network.ts +1 -1
  61. package/src/adapters/cursor/native-exec-shell.ts +0 -1
  62. package/src/adapters/cursor/native-exec.ts +101 -14
  63. package/src/adapters/cursor/protobuf-events.ts +829 -11
  64. package/src/adapters/cursor/protobuf-request.ts +383 -65
  65. package/src/adapters/cursor/request-builder.ts +237 -31
  66. package/src/adapters/cursor/tool-definitions.ts +242 -12
  67. package/src/adapters/cursor/tool-result-normalize.ts +92 -0
  68. package/src/adapters/cursor/transport.ts +22 -0
  69. package/src/adapters/cursor/types.ts +28 -1
  70. package/src/adapters/cursor.ts +133 -7
  71. package/src/adapters/google-antigravity-replay.ts +558 -34
  72. package/src/adapters/google-antigravity-wire.ts +43 -10
  73. package/src/adapters/google-http.ts +38 -10
  74. package/src/adapters/google-tool-schema.ts +80 -15
  75. package/src/adapters/google-truncation.ts +11 -0
  76. package/src/adapters/google.ts +618 -74
  77. package/src/adapters/identity.ts +39 -6
  78. package/src/adapters/kiro-errors.ts +11 -0
  79. package/src/adapters/kiro-events.ts +19 -1
  80. package/src/adapters/kiro-thinking.ts +18 -2
  81. package/src/adapters/kiro-tools.ts +10 -1
  82. package/src/adapters/kiro.ts +92 -54
  83. package/src/adapters/mimo-free.ts +17 -0
  84. package/src/adapters/openai-chat-url.ts +11 -0
  85. package/src/adapters/openai-chat.ts +1311 -336
  86. package/src/adapters/openai-responses-url.ts +16 -0
  87. package/src/adapters/openai-responses.ts +830 -56
  88. package/src/adapters/registry.ts +175 -0
  89. package/src/adapters/responses-tool-schema.ts +67 -0
  90. package/src/adapters/tool-call-id.ts +119 -0
  91. package/src/adapters/tool-catalog-nudge.ts +104 -21
  92. package/src/adapters/xai-web-search.ts +185 -0
  93. package/src/bridge.ts +428 -61
  94. package/src/chat/inbound.ts +43 -19
  95. package/src/chat/outbound.ts +82 -26
  96. package/src/claude/agents-inject.ts +32 -9
  97. package/src/claude/context-windows.ts +21 -5
  98. package/src/claude/desktop-3p.ts +243 -9
  99. package/src/claude/gateway-cache.ts +41 -4
  100. package/src/claude/inbound.ts +72 -3
  101. package/src/claude/model-info.ts +38 -15
  102. package/src/claude/outbound.ts +70 -16
  103. package/src/cli/account-api.ts +35 -1
  104. package/src/cli/account-auth.ts +33 -6
  105. package/src/cli/account-catalog-refresh.ts +14 -0
  106. package/src/cli/account-extended.ts +389 -2
  107. package/src/cli/account-main.ts +317 -0
  108. package/src/cli/account.ts +30 -6
  109. package/src/cli/agent.ts +45 -1
  110. package/src/cli/claude-agent-startup-sync.ts +73 -0
  111. package/src/cli/claude-desktop.ts +29 -4
  112. package/src/cli/claude.ts +103 -34
  113. package/src/cli/codex-log-guard-doctor.ts +103 -0
  114. package/src/cli/codex-shim-autorestore.ts +2 -0
  115. package/src/cli/codex-shim-readiness.ts +76 -0
  116. package/src/cli/combo.ts +8 -0
  117. package/src/cli/config-command.ts +74 -10
  118. package/src/cli/dispatch.ts +593 -0
  119. package/src/cli/doctor.ts +315 -43
  120. package/src/cli/ensure-desired-integrations.ts +152 -0
  121. package/src/cli/export-command.ts +46 -20
  122. package/src/cli/help.ts +20 -271
  123. package/src/cli/index.ts +378 -519
  124. package/src/cli/init.ts +4 -17
  125. package/src/cli/integrations.ts +120 -2
  126. package/src/cli/lab.ts +607 -0
  127. package/src/cli/launcher-context.ts +77 -0
  128. package/src/cli/minimax.ts +497 -0
  129. package/src/cli/models-runtime.ts +35 -2
  130. package/src/cli/models.ts +100 -14
  131. package/src/cli/observe.ts +92 -3
  132. package/src/cli/opencode.ts +4 -2
  133. package/src/cli/provider-runtime.ts +18 -1
  134. package/src/cli/provider.ts +24 -3
  135. package/src/cli/ready.ts +301 -0
  136. package/src/cli/registry.ts +437 -0
  137. package/src/cli/root.ts +86 -0
  138. package/src/cli/route-policy.ts +92 -0
  139. package/src/cli/runtime-api.ts +6 -3
  140. package/src/cli/star-prompt.ts +71 -15
  141. package/src/cli/status.ts +10 -3
  142. package/src/cli/system-restart-client.ts +146 -0
  143. package/src/cli/tray-proxy.ts +153 -6
  144. package/src/cli/v2.ts +105 -10
  145. package/src/cli.ts +1 -1
  146. package/src/clients/config-export.ts +1358 -21
  147. package/src/codex/account-label.ts +14 -1
  148. package/src/codex/account-lifecycle.ts +130 -13
  149. package/src/codex/account-namespaces.ts +49 -3
  150. package/src/codex/account-priority.ts +83 -0
  151. package/src/codex/account-store.ts +29 -2
  152. package/src/codex/account-usability.ts +25 -2
  153. package/src/codex/admission.ts +256 -0
  154. package/src/codex/affinity-debug.ts +162 -0
  155. package/src/codex/app-server-processes.ts +493 -106
  156. package/src/codex/app-server-restart-service.ts +232 -0
  157. package/src/codex/auth-api.ts +849 -242
  158. package/src/codex/auth-collision.ts +5 -3
  159. package/src/codex/auth-context.ts +345 -32
  160. package/src/codex/autostart-health.ts +8 -1
  161. package/src/codex/catalog/account-models.ts +67 -0
  162. package/src/codex/catalog/aggregation.ts +68 -10
  163. package/src/codex/catalog/bundled.ts +331 -33
  164. package/src/codex/catalog/effort.ts +121 -30
  165. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  166. package/src/codex/catalog/kinds.ts +2 -0
  167. package/src/codex/catalog/metadata.ts +529 -45
  168. package/src/codex/catalog/native-models.ts +72 -0
  169. package/src/codex/catalog/parsing.ts +224 -30
  170. package/src/codex/catalog/provider-fetch.ts +1460 -134
  171. package/src/codex/catalog/sync.ts +1449 -186
  172. package/src/codex/catalog-admission.ts +199 -0
  173. package/src/codex/catalog-refresh-status.ts +105 -0
  174. package/src/codex/catalog-write-serialization.ts +242 -0
  175. package/src/codex/catalog.ts +6 -3
  176. package/src/codex/codex-write-lock.ts +384 -0
  177. package/src/codex/convergence-types.ts +614 -0
  178. package/src/codex/convergence.ts +651 -0
  179. package/src/codex/coordinator-doctor.ts +332 -0
  180. package/src/codex/custom-model-catalog-migration.ts +176 -0
  181. package/src/codex/desired-state.ts +230 -0
  182. package/src/codex/features.ts +636 -39
  183. package/src/codex/generation.ts +202 -0
  184. package/src/codex/history-job.ts +407 -0
  185. package/src/codex/history-lock.ts +242 -0
  186. package/src/codex/history-migration-guardian.ts +26 -20
  187. package/src/codex/history-provider.ts +231 -28
  188. package/src/codex/history-transition.ts +105 -0
  189. package/src/codex/history-worker.ts +220 -0
  190. package/src/codex/inject-coordination.ts +290 -0
  191. package/src/codex/inject.ts +1073 -152
  192. package/src/codex/injected-marker.ts +37 -3
  193. package/src/codex/integration-record.ts +266 -0
  194. package/src/codex/internal/catalog-writer.ts +203 -0
  195. package/src/codex/internal/history-writer.ts +80 -0
  196. package/src/codex/journal.ts +66 -4
  197. package/src/codex/log-guard/inspect.ts +506 -0
  198. package/src/codex/log-guard/lock.ts +150 -0
  199. package/src/codex/log-guard/maintenance.ts +403 -0
  200. package/src/codex/log-guard/path-safety.ts +88 -0
  201. package/src/codex/log-guard/policy.ts +44 -0
  202. package/src/codex/log-guard/processes.ts +205 -0
  203. package/src/codex/log-guard/protection.ts +489 -0
  204. package/src/codex/log-guard/sqlite-errors.ts +9 -0
  205. package/src/codex/main-account-cache.ts +24 -0
  206. package/src/codex/main-account.ts +29 -1
  207. package/src/codex/management-convergence.ts +167 -0
  208. package/src/codex/model-cache.ts +56 -10
  209. package/src/codex/model-entitlements.ts +353 -0
  210. package/src/codex/native-main-admission.ts +47 -0
  211. package/src/codex/native-main-auth-temp.ts +187 -0
  212. package/src/codex/native-main-claim.ts +178 -0
  213. package/src/codex/native-main-lock-file.ts +162 -0
  214. package/src/codex/native-main-owner.ts +329 -0
  215. package/src/codex/native-profile-api.ts +247 -0
  216. package/src/codex/native-profile-manager.ts +1531 -0
  217. package/src/codex/native-profile-processes.ts +121 -0
  218. package/src/codex/native-profile-recovery.ts +99 -0
  219. package/src/codex/native-profile-stage-store.ts +387 -0
  220. package/src/codex/native-profile-startup.ts +492 -0
  221. package/src/codex/native-profile-store.ts +855 -0
  222. package/src/codex/native-profile-types.ts +120 -0
  223. package/src/codex/native-residue.ts +682 -0
  224. package/src/codex/paths.ts +80 -1
  225. package/src/codex/plan-from-token.ts +140 -0
  226. package/src/codex/plan.ts +40 -0
  227. package/src/codex/plugins-doctor.ts +1 -1
  228. package/src/codex/pool-rotation.ts +74 -4
  229. package/src/codex/project-config-warnings.ts +20 -6
  230. package/src/codex/prompt-journal.ts +352 -0
  231. package/src/codex/prompt-layers.ts +967 -0
  232. package/src/codex/prompt-lock.ts +143 -0
  233. package/src/codex/quota-rejection.ts +298 -0
  234. package/src/codex/quota.ts +175 -13
  235. package/src/codex/refresh.ts +11 -2
  236. package/src/codex/reset-credit-recovery.ts +1044 -0
  237. package/src/codex/routing.ts +505 -94
  238. package/src/codex/runtime.ts +159 -38
  239. package/src/codex/shim.ts +1009 -28
  240. package/src/codex/subagent-model-fallback.ts +350 -35
  241. package/src/codex/sync.ts +191 -2
  242. package/src/codex/transition-state.ts +612 -0
  243. package/src/codex/upstream-host-health.ts +368 -0
  244. package/src/codex/user-identity.ts +557 -0
  245. package/src/codex/warmup.ts +187 -81
  246. package/src/codex/write-coordination.ts +114 -0
  247. package/src/combos/failover.ts +20 -0
  248. package/src/combos/index.ts +4 -0
  249. package/src/combos/request.ts +32 -0
  250. package/src/combos/types.ts +81 -9
  251. package/src/config/provider-name.ts +24 -0
  252. package/src/config.ts +1762 -140
  253. package/src/generated/compatibility-version.json +3164 -0
  254. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +19 -17
  255. package/src/grok/inject.ts +16 -5
  256. package/src/grok/inspect.ts +45 -0
  257. package/src/grok/sync.ts +2 -2
  258. package/src/images/loop.ts +152 -29
  259. package/src/images/plan.ts +23 -13
  260. package/src/integrations/config-io.ts +269 -0
  261. package/src/integrations/journal.ts +315 -0
  262. package/src/integrations/merge.ts +135 -0
  263. package/src/integrations/mutation-flight.ts +71 -0
  264. package/src/integrations/native/ownership-preflight.ts +202 -0
  265. package/src/integrations/omp-yaml-source.ts +358 -0
  266. package/src/integrations/owned-refresh.ts +74 -0
  267. package/src/integrations/ownership.ts +111 -0
  268. package/src/integrations/registry.ts +159 -0
  269. package/src/integrations/serialize.ts +314 -0
  270. package/src/integrations/state.ts +361 -0
  271. package/src/integrations/store.ts +103 -0
  272. package/src/integrations/writer-lock.ts +98 -0
  273. package/src/integrations/writer.ts +691 -0
  274. package/src/lab/artifacts/sanitize.ts +586 -0
  275. package/src/lab/artifacts/secure-fs.ts +475 -0
  276. package/src/lab/artifacts/store.ts +310 -0
  277. package/src/lab/automation/budgets.ts +78 -0
  278. package/src/lab/automation/config-persistence.ts +256 -0
  279. package/src/lab/automation/constants.ts +39 -0
  280. package/src/lab/automation/cooldown.ts +103 -0
  281. package/src/lab/automation/dispatch.ts +211 -0
  282. package/src/lab/automation/index.ts +13 -0
  283. package/src/lab/automation/orchestrator.ts +499 -0
  284. package/src/lab/automation/persistence.ts +512 -0
  285. package/src/lab/automation/planner.ts +371 -0
  286. package/src/lab/automation/policy.ts +136 -0
  287. package/src/lab/automation/queue.ts +191 -0
  288. package/src/lab/automation/recovery.ts +24 -0
  289. package/src/lab/automation/route-context.ts +21 -0
  290. package/src/lab/automation/run-key.ts +44 -0
  291. package/src/lab/automation/runs-query.ts +34 -0
  292. package/src/lab/automation/types.ts +160 -0
  293. package/src/lab/conformance/assertion.ts +325 -0
  294. package/src/lab/conformance/digest.ts +22 -0
  295. package/src/lab/conformance/executor.ts +741 -0
  296. package/src/lab/conformance/fixture-provider.ts +27 -0
  297. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  298. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  299. package/src/lab/conformance/harness-budget.ts +47 -0
  300. package/src/lab/conformance/index.ts +5 -0
  301. package/src/lab/conformance/jcs.ts +64 -0
  302. package/src/lab/conformance/json-pointer.ts +39 -0
  303. package/src/lab/conformance/manifest.ts +180 -0
  304. package/src/lab/conformance/mcp-stub.ts +179 -0
  305. package/src/lab/conformance/negative-controls.ts +164 -0
  306. package/src/lab/conformance/observation.ts +355 -0
  307. package/src/lab/conformance/runner.ts +68 -0
  308. package/src/lab/conformance/sse-normalize.ts +59 -0
  309. package/src/lab/conformance/suite-manifest.ts +78 -0
  310. package/src/lab/conformance/types.ts +214 -0
  311. package/src/lab/constants.ts +126 -0
  312. package/src/lab/digest.ts +64 -0
  313. package/src/lab/events/errors.ts +9 -0
  314. package/src/lab/events/limits.ts +117 -0
  315. package/src/lab/events/types.ts +229 -0
  316. package/src/lab/events/validate.ts +781 -0
  317. package/src/lab/fabric/constants.ts +40 -0
  318. package/src/lab/fabric/executor.ts +492 -0
  319. package/src/lab/fabric/index.ts +80 -0
  320. package/src/lab/fabric/manifest.ts +222 -0
  321. package/src/lab/fabric/observe.ts +489 -0
  322. package/src/lab/fabric/patch.ts +79 -0
  323. package/src/lab/fabric/producer-child.ts +139 -0
  324. package/src/lab/fabric/producer-isolate.ts +276 -0
  325. package/src/lab/fabric/producer-protocol.ts +61 -0
  326. package/src/lab/fabric/scratch.ts +439 -0
  327. package/src/lab/fabric/subject.ts +106 -0
  328. package/src/lab/fabric/types.ts +134 -0
  329. package/src/lab/fabric/verifier.ts +98 -0
  330. package/src/lab/index.ts +54 -0
  331. package/src/lab/ledger/artifact-refs.ts +127 -0
  332. package/src/lab/ledger/invalidation.ts +136 -0
  333. package/src/lab/ledger/purge.ts +310 -0
  334. package/src/lab/ledger/store.ts +532 -0
  335. package/src/lab/live/credential-lease.ts +53 -0
  336. package/src/lab/live/destination.ts +155 -0
  337. package/src/lab/live/executor.ts +336 -0
  338. package/src/lab/live/inert-tools.ts +56 -0
  339. package/src/lab/live/manifest.ts +85 -0
  340. package/src/lab/live/mcp-loopback.ts +57 -0
  341. package/src/lab/live/runner.ts +19 -0
  342. package/src/lab/live/sandbox.ts +61 -0
  343. package/src/lab/live/suite-manifest.ts +41 -0
  344. package/src/lab/live/transport.ts +118 -0
  345. package/src/lab/live/types.ts +197 -0
  346. package/src/lab/observe/from-conformance.ts +301 -0
  347. package/src/lab/observe/from-live.ts +117 -0
  348. package/src/lab/paths.ts +153 -0
  349. package/src/lab/projection/rebuild.ts +495 -0
  350. package/src/lab/projection/schema.ts +135 -0
  351. package/src/lab/projection/verdicts.ts +474 -0
  352. package/src/lab/projection/verification.ts +412 -0
  353. package/src/lab/public/bundle.ts +217 -0
  354. package/src/lab/public/community-authority.ts +175 -0
  355. package/src/lab/public/community-files.ts +29 -0
  356. package/src/lab/public/community.ts +479 -0
  357. package/src/lab/public/file-safety.ts +155 -0
  358. package/src/lab/public/ids.ts +26 -0
  359. package/src/lab/public/index.ts +16 -0
  360. package/src/lab/public/mutation-lock.ts +424 -0
  361. package/src/lab/public/operator.ts +353 -0
  362. package/src/lab/public/origin-purge.ts +79 -0
  363. package/src/lab/public/origin.ts +203 -0
  364. package/src/lab/public/privacy.ts +143 -0
  365. package/src/lab/public/private-file.ts +261 -0
  366. package/src/lab/public/project.ts +124 -0
  367. package/src/lab/public/purge-test-fault.ts +21 -0
  368. package/src/lab/public/purge.ts +223 -0
  369. package/src/lab/public/registry.ts +44 -0
  370. package/src/lab/public/revocation.ts +252 -0
  371. package/src/lab/public/signature.ts +243 -0
  372. package/src/lab/public/storage.ts +105 -0
  373. package/src/lab/public/strict-json.ts +206 -0
  374. package/src/lab/public/time.ts +26 -0
  375. package/src/lab/public/types.ts +172 -0
  376. package/src/lab/public/validate.ts +391 -0
  377. package/src/lab/query/catalog.ts +101 -0
  378. package/src/lab/query/connection.ts +107 -0
  379. package/src/lab/query/constants.ts +4 -0
  380. package/src/lab/query/cursor.ts +132 -0
  381. package/src/lab/query/dto-map.ts +277 -0
  382. package/src/lab/query/errors.ts +22 -0
  383. package/src/lab/query/freshness.ts +53 -0
  384. package/src/lab/query/index.ts +45 -0
  385. package/src/lab/query/latest-observation.ts +59 -0
  386. package/src/lab/query/passive-production.ts +159 -0
  387. package/src/lab/query/queries.ts +444 -0
  388. package/src/lab/query/types.ts +266 -0
  389. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  390. package/src/lab/subject/installation-salt.ts +112 -0
  391. package/src/lab/subject/protocol-subject.ts +80 -0
  392. package/src/lab/subject/route-subject.ts +74 -0
  393. package/src/lib/app-owned-memory-stores.ts +22 -0
  394. package/src/lib/bounded-body.ts +153 -9
  395. package/src/lib/bun-runtime.ts +125 -12
  396. package/src/lib/bun-stream-caps.ts +13 -9
  397. package/src/lib/codex-restart-contract.ts +120 -0
  398. package/src/lib/config-ownership.ts +6 -2
  399. package/src/lib/destination-policy.ts +65 -1
  400. package/src/lib/errors.ts +44 -2
  401. package/src/lib/fabric-task-execution-authority.ts +7 -0
  402. package/src/lib/fabric-task-host.ts +29 -0
  403. package/src/lib/lab-activation.ts +223 -0
  404. package/src/lib/lab-live-execution-authority.ts +13 -0
  405. package/src/lib/lab-live-host.ts +30 -0
  406. package/src/lib/lab-live-pinned-sender.ts +56 -0
  407. package/src/lib/lab-live-route-production.ts +130 -0
  408. package/src/lib/lab-passive-linker-registration.ts +26 -0
  409. package/src/lib/local-management-attestation.ts +51 -0
  410. package/src/lib/local-management-capability.ts +100 -0
  411. package/src/lib/local-provider-reload-contract.ts +100 -0
  412. package/src/lib/optional-shutdown-hooks.ts +57 -0
  413. package/src/lib/pinned-http.ts +145 -26
  414. package/src/lib/process-control.ts +4 -1
  415. package/src/lib/provider-outbound.ts +49 -9
  416. package/src/lib/redact.ts +419 -3
  417. package/src/lib/self-launch-argv.ts +15 -0
  418. package/src/lib/server-resource-ownership.ts +71 -0
  419. package/src/lib/shadow-call.ts +35 -4
  420. package/src/lib/sse-decoder.ts +41 -0
  421. package/src/lib/state-store-registrations.ts +10 -2
  422. package/src/lib/system-restart-contract.ts +73 -0
  423. package/src/lib/token-estimate.ts +19 -2
  424. package/src/lib/tool-argument-integers.ts +202 -0
  425. package/src/lib/translator-budget.ts +44 -0
  426. package/src/lib/upstream-http-version.ts +57 -0
  427. package/src/lib/upstream-reachability.ts +95 -0
  428. package/src/lib/upstream-retry.ts +156 -3
  429. package/src/lib/windows-atomic-replace.ts +155 -0
  430. package/src/lib/windows-elevation.ts +70 -2
  431. package/src/lib/windows-secret-acl.ts +409 -69
  432. package/src/lib/windows-service-wrappers.ts +72 -0
  433. package/src/lib/windows-text.ts +106 -0
  434. package/src/lib/windows-user-principal.ts +341 -0
  435. package/src/lib/winsw.ts +33 -5
  436. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  437. package/src/oauth/account-import/index.ts +15 -0
  438. package/src/oauth/account-import/parser.ts +83 -0
  439. package/src/oauth/account-import/registry.ts +18 -0
  440. package/src/oauth/account-import/service.ts +75 -0
  441. package/src/oauth/account-import/types.ts +91 -0
  442. package/src/oauth/anthropic.ts +12 -1
  443. package/src/oauth/callback-server.ts +8 -2
  444. package/src/oauth/chatgpt.ts +12 -1
  445. package/src/oauth/command-code.ts +239 -0
  446. package/src/oauth/cursor.ts +46 -5
  447. package/src/oauth/google-antigravity.ts +35 -3
  448. package/src/oauth/health.ts +20 -12
  449. package/src/oauth/index.ts +398 -66
  450. package/src/oauth/key-providers.ts +16 -0
  451. package/src/oauth/kimi.ts +16 -2
  452. package/src/oauth/kiro.ts +50 -6
  453. package/src/oauth/local-token-detect.ts +11 -2
  454. package/src/oauth/log.ts +3 -1
  455. package/src/oauth/login-cli.ts +88 -28
  456. package/src/oauth/nous.ts +798 -0
  457. package/src/oauth/store.ts +119 -21
  458. package/src/oauth/token-guardian.ts +9 -3
  459. package/src/pi/models.ts +2 -2
  460. package/src/providers/alibaba-region-migration.ts +1 -1
  461. package/src/providers/antigravity-models.ts +521 -31
  462. package/src/providers/base-url-choices.ts +10 -0
  463. package/src/providers/codex-capacity.ts +292 -0
  464. package/src/providers/command-code-efforts.ts +144 -0
  465. package/src/providers/context-cap.ts +22 -5
  466. package/src/providers/cursor-pool.ts +72 -0
  467. package/src/providers/derive.ts +253 -6
  468. package/src/providers/fastwire.ts +501 -0
  469. package/src/providers/free-directory.ts +10 -7
  470. package/src/providers/google-vertex-location.ts +14 -0
  471. package/src/providers/key-failover.ts +71 -3
  472. package/src/providers/label.ts +1 -1
  473. package/src/providers/model-discovery-limits.ts +16 -0
  474. package/src/providers/model-discovery.ts +115 -22
  475. package/src/providers/model-rename-migration.ts +255 -0
  476. package/src/providers/model-rename-startup.ts +28 -0
  477. package/src/providers/openai-sidecar.ts +72 -4
  478. package/src/providers/openai-tier-startup.ts +31 -2
  479. package/src/providers/openai-tiers.ts +119 -4
  480. package/src/providers/openai-virtual-models.ts +1 -0
  481. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  482. package/src/providers/provider-id-rewrite.ts +29 -0
  483. package/src/providers/quota.ts +1319 -38
  484. package/src/providers/registry.ts +1429 -111
  485. package/src/providers/request-pacing.ts +310 -0
  486. package/src/providers/service-tier.ts +277 -0
  487. package/src/providers/slug-codec.ts +42 -6
  488. package/src/providers/static-model-discovery.ts +86 -0
  489. package/src/providers/xai-responses-opt-in.ts +15 -0
  490. package/src/providers/xai-transport.ts +11 -4
  491. package/src/reasoning-effort.ts +49 -1
  492. package/src/responses/compaction.ts +26 -1
  493. package/src/responses/custom-tool-compat.ts +266 -0
  494. package/src/responses/hosted-tool-policy.ts +9 -0
  495. package/src/responses/namespace-tool-compat.ts +355 -0
  496. package/src/responses/parser.ts +220 -38
  497. package/src/responses/provider-continuation.ts +98 -0
  498. package/src/responses/provider-opaque-metadata.ts +73 -0
  499. package/src/responses/reasoning-envelope.ts +9 -1
  500. package/src/responses/reasoning-replay-cache.ts +426 -0
  501. package/src/responses/schema.ts +7 -1
  502. package/src/responses/spill-store.ts +75 -10
  503. package/src/responses/state.ts +565 -27
  504. package/src/responses/thought-signature-replay.ts +347 -0
  505. package/src/responses/tool-search-compat.ts +301 -0
  506. package/src/responses/truncated-stop-reason.ts +60 -0
  507. package/src/router.ts +366 -30
  508. package/src/routing/analytics.ts +378 -0
  509. package/src/routing/capability.ts +244 -0
  510. package/src/routing/compatibility/assemble.ts +73 -0
  511. package/src/routing/compatibility/behavior.ts +278 -0
  512. package/src/routing/compatibility/catalog.ts +99 -0
  513. package/src/routing/compatibility/endpoint.ts +52 -0
  514. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  515. package/src/routing/compatibility/policy.ts +181 -0
  516. package/src/routing/compatibility/provider-slot.ts +56 -0
  517. package/src/routing/compatibility/reader.ts +110 -0
  518. package/src/routing/compatibility/subject.ts +191 -0
  519. package/src/routing/compatibility/types.ts +64 -0
  520. package/src/routing/compatibility/version.ts +104 -0
  521. package/src/routing/cost.ts +77 -0
  522. package/src/routing/evaluator.ts +495 -0
  523. package/src/routing/health.ts +412 -0
  524. package/src/routing/history/cursor.ts +43 -0
  525. package/src/routing/history/indexer.ts +605 -0
  526. package/src/routing/history/schema.ts +72 -0
  527. package/src/routing/profile-namespace.ts +15 -0
  528. package/src/routing/profile.ts +547 -0
  529. package/src/routing/quota.ts +145 -0
  530. package/src/routing/request-evidence.ts +45 -0
  531. package/src/routing/trace.ts +776 -0
  532. package/src/server/adapter-resolve.ts +2 -29
  533. package/src/server/auth-cors.ts +267 -46
  534. package/src/server/background-lifecycle.ts +182 -0
  535. package/src/server/chat-completions.ts +130 -56
  536. package/src/server/chat-native-sse.ts +331 -0
  537. package/src/server/chat-native.ts +426 -0
  538. package/src/server/claude-messages.ts +159 -43
  539. package/src/server/direct-local-http.ts +347 -0
  540. package/src/server/effort-policy.ts +18 -0
  541. package/src/server/github-copilot-responses-repair.ts +338 -0
  542. package/src/server/gui-static.ts +39 -10
  543. package/src/server/images.ts +94 -12
  544. package/src/server/index.ts +865 -181
  545. package/src/server/lifecycle.ts +284 -13
  546. package/src/server/live.ts +136 -17
  547. package/src/server/local-management-read-client.ts +90 -0
  548. package/src/server/local-provider-reload-client.ts +137 -0
  549. package/src/server/management/agent-settings-routes.ts +398 -103
  550. package/src/server/management/api-key-usage.ts +31 -5
  551. package/src/server/management/body.ts +6 -0
  552. package/src/server/management/combo-routes.ts +62 -24
  553. package/src/server/management/config-routes.ts +464 -51
  554. package/src/server/management/context.ts +80 -2
  555. package/src/server/management/integration-routes.ts +498 -0
  556. package/src/server/management/lab-automation-routes.ts +206 -0
  557. package/src/server/management/lab-routes.ts +563 -0
  558. package/src/server/management/logs-usage-routes.ts +101 -32
  559. package/src/server/management/model-routes.ts +189 -131
  560. package/src/server/management/model-rows.ts +163 -0
  561. package/src/server/management/native-integration-routes.ts +769 -0
  562. package/src/server/management/oauth-account-routes.ts +80 -4
  563. package/src/server/management/provider-capability-config.ts +48 -0
  564. package/src/server/management/provider-routes.ts +764 -157
  565. package/src/server/management/request-history-routes.ts +191 -0
  566. package/src/server/management/routing-analytics-routes.ts +74 -0
  567. package/src/server/management/routing-profile-routes.ts +380 -0
  568. package/src/server/management/shared.ts +27 -11
  569. package/src/server/management/sidebar-routes.ts +47 -31
  570. package/src/server/management/storage-log-guard-routes.ts +186 -0
  571. package/src/server/management/sync-response.ts +69 -0
  572. package/src/server/management/system-restart.ts +288 -32
  573. package/src/server/management/system-routes.ts +77 -0
  574. package/src/server/management/usage-summary-cache.ts +9 -1
  575. package/src/server/management/vision-sidecar-options.ts +167 -0
  576. package/src/server/management/web-search-sidecar-options.ts +120 -0
  577. package/src/server/management-api.ts +115 -14
  578. package/src/server/management-auth.ts +220 -5
  579. package/src/server/passive-route-linker.ts +66 -0
  580. package/src/server/ports.ts +41 -1
  581. package/src/server/proxy-liveness.ts +132 -5
  582. package/src/server/readiness.ts +99 -0
  583. package/src/server/relay-eager.ts +82 -42
  584. package/src/server/relay.ts +236 -76
  585. package/src/server/request-decompress.ts +113 -6
  586. package/src/server/request-log.ts +235 -22
  587. package/src/server/responses/agent-task-recovery-cache.ts +143 -0
  588. package/src/server/responses/agent-task-recovery.ts +465 -0
  589. package/src/server/responses/collaboration.ts +204 -35
  590. package/src/server/responses/compact.ts +442 -55
  591. package/src/server/responses/core.ts +2872 -331
  592. package/src/server/responses/empty-completion-guard.ts +276 -0
  593. package/src/server/responses/encrypted-payload.ts +62 -39
  594. package/src/server/responses/fetch-helpers.ts +79 -4
  595. package/src/server/responses/input-admission.ts +185 -0
  596. package/src/server/responses/pacing-overload.ts +13 -0
  597. package/src/server/responses/policy-fallback.ts +178 -0
  598. package/src/server/responses/responses-field-backfill.ts +251 -0
  599. package/src/server/responses/terminal-guard.ts +26 -5
  600. package/src/server/responses/upstream-error.ts +5 -0
  601. package/src/server/responses/ws-upstream.ts +308 -0
  602. package/src/server/responses-custom-tool-repair.ts +282 -0
  603. package/src/server/responses-item-id-repair.ts +54 -6
  604. package/src/server/responses-json-events.ts +90 -0
  605. package/src/server/responses-model-rewrite.ts +29 -0
  606. package/src/server/responses-reasoning-summary-rewrite.ts +178 -0
  607. package/src/server/responses-snapshot-repair.ts +621 -0
  608. package/src/server/responses-terminal-repair.ts +342 -0
  609. package/src/server/responses-tool-search-repair.ts +267 -0
  610. package/src/server/responses-undeclared-tool-guard.ts +153 -0
  611. package/src/server/responses.ts +18 -2
  612. package/src/server/search.ts +78 -13
  613. package/src/server/sse-frame-buffer.ts +292 -0
  614. package/src/server/sse-payload-rewrite.ts +110 -22
  615. package/src/server/startup-action-control.ts +8 -1
  616. package/src/server/startup-health-cache.ts +19 -1
  617. package/src/server/system-env.ts +80 -9
  618. package/src/server/ws-bridge.ts +39 -38
  619. package/src/service-manager-probe.ts +892 -0
  620. package/src/service.ts +1111 -90
  621. package/src/sidecar/auth.ts +92 -0
  622. package/src/sidecar/candidates.ts +83 -0
  623. package/src/storage/cleanup.ts +2 -2
  624. package/src/storage/scanner.ts +1 -1
  625. package/src/storage/worker-lifecycle.ts +14 -14
  626. package/src/tray/windows-tray.ps1 +83 -9
  627. package/src/tray/windows.ts +43 -16
  628. package/src/types/accounts.ts +37 -0
  629. package/src/types/config.ts +845 -0
  630. package/src/types/provider.ts +545 -0
  631. package/src/types/request.ts +384 -0
  632. package/src/types/tools.ts +131 -0
  633. package/src/types/wire.ts +80 -0
  634. package/src/types.ts +104 -1236
  635. package/src/update/index.ts +32 -19
  636. package/src/update/job.ts +442 -67
  637. package/src/update/notify.ts +12 -6
  638. package/src/update/npm-cache-preflight.d.mts +47 -0
  639. package/src/update/npm-cache-preflight.mjs +201 -0
  640. package/src/update/transactional-install.d.mts +22 -0
  641. package/src/update/transactional-install.mjs +259 -0
  642. package/src/usage/cost.ts +0 -0
  643. package/src/usage/expected-prices.ts +268 -16
  644. package/src/usage/log.ts +606 -41
  645. package/src/usage/summary.ts +177 -9
  646. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  647. package/src/usage/user-cost-overlays.ts +314 -0
  648. package/src/vision/anthropic-describe.ts +10 -6
  649. package/src/vision/backends.ts +97 -0
  650. package/src/vision/describe.ts +9 -3
  651. package/src/vision/eligibility.ts +250 -0
  652. package/src/vision/index.ts +238 -24
  653. package/src/vision/reasoning.ts +55 -0
  654. package/src/vision/routed-describe.ts +175 -0
  655. package/src/vision/timeout-bounds.ts +9 -0
  656. package/src/web-search/anthropic-executor.ts +13 -7
  657. package/src/web-search/backends.ts +108 -0
  658. package/src/web-search/exa-executor.ts +88 -0
  659. package/src/web-search/executor.ts +11 -3
  660. package/src/web-search/gemini-executor.ts +141 -0
  661. package/src/web-search/index.ts +150 -15
  662. package/src/web-search/loop.ts +279 -50
  663. package/src/web-search/parse.ts +125 -30
  664. package/src/web-search/sources.ts +60 -0
  665. package/src/web-search/xai-executor.ts +219 -0
  666. package/gui/dist/assets/index-DTpMHS4F.js +0 -67
  667. package/gui/dist/assets/index-ZNVDE3C7.css +0 -1
  668. package/gui/dist/provider-icons/antigravity.svg +0 -1
  669. package/gui/dist/provider-icons/claude.svg +0 -1
  670. package/gui/dist/provider-icons/copilot.svg +0 -1
  671. package/gui/dist/provider-icons/cursor.svg +0 -2
  672. package/gui/dist/provider-icons/gemini.svg +0 -1
  673. package/gui/dist/provider-icons/grok-color.svg +0 -1
  674. package/gui/dist/provider-icons/kiro.svg +0 -14
  675. package/src/cli/internal-dispatch.ts +0 -20
@@ -0,0 +1,776 @@
1
+ /**
2
+ * Route decision trace: bounded, versioned, privacy-safe evidence of WHY a
3
+ * provider/model/account was selected for a request (RI-01).
4
+ *
5
+ * Contract rules (devlog/_fin/260804_router_intelligence/000_master_plan.md):
6
+ * - One trace per routing decision; fallback EXECUTION attempts stay in the
7
+ * usage entry's existing `attempts[]` array, never in this trace.
8
+ * - Never persists prompts, message bodies, tool payloads, credentials,
9
+ * authorization headers, raw quota responses, or hidden reasoning.
10
+ * - Stable wire values only; no localized strings.
11
+ * - Deterministic bounds: candidates <= 8, exclusions per candidate <= 16,
12
+ * requirements <= 16, strings <= 128 chars, serialized trace <= 16 KiB.
13
+ * - Truncation is explicit: `truncated` flags are set whenever a limit was
14
+ * enforced.
15
+ */
16
+
17
+ import { randomBytes } from "node:crypto";
18
+
19
+ export type RouteDecisionKind =
20
+ | "explicit-account"
21
+ | "explicit-provider"
22
+ | "native"
23
+ | "combo"
24
+ | "policy"
25
+ | "default-provider";
26
+
27
+ export type Unknownable = number | boolean | "unknown";
28
+
29
+ export interface RouteRequirementEvidence {
30
+ /** Stable wire code, e.g. "min-context-window" | "tools" | "image-input". */
31
+ id: string;
32
+ expected?: string | number | boolean;
33
+ actual?: Unknownable | string;
34
+ outcome: "satisfied" | "unsatisfied" | "unknown";
35
+ }
36
+
37
+ export interface RouteExclusionReason {
38
+ /** Stable wire code, e.g. "cooldown" | "disabled" | "cost-limit". */
39
+ code: string;
40
+ detail?: string;
41
+ }
42
+
43
+ export interface RouteCapabilityEvidence {
44
+ contextWindow?: number;
45
+ tools?: Unknownable;
46
+ image?: Unknownable;
47
+ structuredOutput?: Unknownable;
48
+ reasoningEfforts?: string[];
49
+ /** `"unknown"` is reserved: it encodes missing evidence, never a real tier. */
50
+ serviceTier?: string | "unknown";
51
+ localOnly?: Unknownable;
52
+ remoteAllowed?: Unknownable;
53
+ encryptedCodexTasks?: Unknownable;
54
+ }
55
+
56
+ export interface RouteHealthEvidence {
57
+ cooldownUntilMs?: number;
58
+ softAvoidUntilMs?: number;
59
+ successRate?: number;
60
+ failures?: number;
61
+ incompleteStreamRate?: number;
62
+ recentLatencyMs?: number;
63
+ sampleCount?: number;
64
+ recencyWeight?: number;
65
+ }
66
+
67
+ export interface RouteQuotaEvidence {
68
+ known: boolean;
69
+ /** Remaining headroom as a fraction 0..1 (larger is better). */
70
+ headroom?: number;
71
+ headroomTokens?: number;
72
+ exhausted?: boolean;
73
+ resetAtMs?: number;
74
+ reauthOrCooling?: boolean;
75
+ reservedHeadroomTokens?: number;
76
+ /** Stable wire code for the evidence source (e.g. "provider-report"). */
77
+ source?: string;
78
+ }
79
+
80
+ /**
81
+ * Stable wire outcome for `limits.maxEstimatedCostUsd` against this candidate's
82
+ * cost evidence. Present only when a cap is configured. Non-excluding outcomes
83
+ * (`satisfied`, `unknown-allowed`) must not be mirrored into `exclusions`.
84
+ */
85
+ export type RouteCostCapOutcome =
86
+ | "satisfied"
87
+ | "exceeded"
88
+ | "unknown-allowed"
89
+ | "unknown-excluded";
90
+
91
+ export interface RouteCostEvidence {
92
+ estimatedUsd?: number;
93
+ /** Stable wire code for the price source (e.g. "registry" | "expected"). */
94
+ priceSource?: string;
95
+ incomplete?: boolean;
96
+ limitUsd?: number;
97
+ /**
98
+ * Cap evaluation outcome when `limitUsd` / profile `maxEstimatedCostUsd` is set.
99
+ * Distinguishes "known under the cap" from "unknown cost allowed by policy"
100
+ * without treating the latter as an exclusion.
101
+ */
102
+ capOutcome?: RouteCostCapOutcome;
103
+ }
104
+
105
+ export interface RouteCompatibilitySuiteTrace {
106
+ suiteId: string;
107
+ evidenceLayer: string;
108
+ /** Privacy-safe prefix of the exact subject used for this layer/suite. */
109
+ subjectIdPrefix?: string;
110
+ verdict?: string;
111
+ minStatus?: string;
112
+ fresh?: boolean;
113
+ unknownPolicy?: string;
114
+ degradedPolicy?: string;
115
+ outcome: "satisfied" | "penalized" | "excluded" | "unknown";
116
+ reason?: string;
117
+ }
118
+
119
+ export interface RouteCompatibilityEvidence {
120
+ /** Legacy single-subject prefix retained for persisted trace compatibility. */
121
+ subjectIdPrefix?: string;
122
+ suites: RouteCompatibilitySuiteTrace[];
123
+ truncated?: true;
124
+ }
125
+
126
+ export interface RouteScoreEvidence {
127
+ total: number;
128
+ components: {
129
+ capability?: number;
130
+ health?: number;
131
+ quota?: number;
132
+ cost?: number;
133
+ latency?: number;
134
+ configuredPriority?: number;
135
+ compatibility?: number;
136
+ };
137
+ }
138
+
139
+ export interface RouteCandidateTrace {
140
+ provider: string;
141
+ model: string;
142
+ accountRef?: string;
143
+ eligible: boolean;
144
+ exclusions: RouteExclusionReason[];
145
+ capability?: RouteCapabilityEvidence;
146
+ health?: RouteHealthEvidence;
147
+ quota?: RouteQuotaEvidence;
148
+ cost?: RouteCostEvidence;
149
+ compatibility?: RouteCompatibilityEvidence;
150
+ score?: RouteScoreEvidence;
151
+ }
152
+
153
+ export interface RouteDecisionTraceV1 {
154
+ version: 1;
155
+ decisionId: string;
156
+ createdAt: number;
157
+ requestedModel: string;
158
+ routeKind: RouteDecisionKind;
159
+ profile?: { id: string; revision: string };
160
+ requirements: RouteRequirementEvidence[];
161
+ candidates: RouteCandidateTrace[];
162
+ selected: {
163
+ candidateIndex: number;
164
+ provider: string;
165
+ model: string;
166
+ accountRef?: string;
167
+ reason: string;
168
+ tieBreak?: string;
169
+ };
170
+ truncated?: {
171
+ candidates?: true;
172
+ exclusions?: true;
173
+ requirements?: true;
174
+ strings?: true;
175
+ compatibility?: true;
176
+ };
177
+ }
178
+
179
+ export const MAX_TRACE_CANDIDATES = 8;
180
+ export const MAX_EXCLUSIONS_PER_CANDIDATE = 16;
181
+ export const MAX_REQUIREMENTS = 16;
182
+ export const MAX_TRACE_STRING = 128;
183
+ export const MAX_TRACE_BYTES = 16 * 1024;
184
+
185
+ const ROUTE_KINDS = new Set<RouteDecisionKind>([
186
+ "explicit-account",
187
+ "explicit-provider",
188
+ "native",
189
+ "combo",
190
+ "policy",
191
+ "default-provider",
192
+ ]);
193
+
194
+ const REQUIREMENT_OUTCOMES = new Set(["satisfied", "unsatisfied", "unknown"]);
195
+
196
+ /** Cap a string at MAX_TRACE_STRING and record the truncation flag. */
197
+ function capString(value: string, budget: { strings?: true }): string {
198
+ if (value.length <= MAX_TRACE_STRING) return value;
199
+ budget.strings = true;
200
+ return value.slice(0, MAX_TRACE_STRING);
201
+ }
202
+
203
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
204
+ return typeof value === "object" && value !== null && !Array.isArray(value);
205
+ }
206
+
207
+ function finiteNumber(value: unknown): value is number {
208
+ return typeof value === "number" && Number.isFinite(value);
209
+ }
210
+
211
+ function unknownable(value: unknown): Unknownable | undefined {
212
+ if (typeof value === "boolean") return value;
213
+ if (finiteNumber(value)) return value;
214
+ if (value === "unknown") return "unknown";
215
+ return undefined;
216
+ }
217
+
218
+ export interface TraceCandidateInput {
219
+ provider: string;
220
+ model: string;
221
+ accountRef?: string;
222
+ eligible: boolean;
223
+ exclusions: RouteExclusionReason[];
224
+ score?: RouteScoreEvidence;
225
+ capability?: RouteCapabilityEvidence;
226
+ health?: RouteHealthEvidence;
227
+ quota?: RouteQuotaEvidence;
228
+ cost?: RouteCostEvidence;
229
+ compatibility?: RouteCompatibilityEvidence;
230
+ }
231
+
232
+ export interface TraceBuildInput {
233
+ requestedModel: string;
234
+ routeKind: RouteDecisionKind;
235
+ selected: {
236
+ provider: string;
237
+ model: string;
238
+ accountRef?: string;
239
+ reason: string;
240
+ tieBreak?: string;
241
+ /** Index into `candidates`; defaults to 0. */
242
+ candidateIndex?: number;
243
+ };
244
+ profile?: { id: string; revision: string };
245
+ requirements?: RouteRequirementEvidence[];
246
+ candidates?: TraceCandidateInput[];
247
+ now?: number;
248
+ }
249
+
250
+ interface ParseCaps {
251
+ candidates?: true;
252
+ exclusions?: true;
253
+ requirements?: true;
254
+ strings?: true;
255
+ compatibility?: true;
256
+ }
257
+
258
+ /** Bounded candidate copy: strings capped, exclusions sliced, score/evidence kept. */
259
+ function buildCandidate(input: TraceCandidateInput, budget: ParseCaps): RouteCandidateTrace {
260
+ const exclusions = input.exclusions.slice(0, MAX_EXCLUSIONS_PER_CANDIDATE);
261
+ if (exclusions.length < input.exclusions.length) budget.exclusions = true;
262
+ const capability = input.capability ? parseCapability(input.capability, budget) : undefined;
263
+ const health = input.health ? parseHealth(input.health) : undefined;
264
+ const quota = input.quota ? parseQuota(input.quota, budget) : undefined;
265
+ const cost = input.cost ? parseCost(input.cost, budget) : undefined;
266
+ const compatibility = input.compatibility ? parseCompatibility(input.compatibility, budget) : undefined;
267
+ return {
268
+ provider: capString(input.provider, budget),
269
+ model: capString(input.model, budget),
270
+ ...(input.accountRef !== undefined
271
+ ? { accountRef: capString(input.accountRef, budget) }
272
+ : {}),
273
+ eligible: input.eligible,
274
+ exclusions: exclusions.map(exclusion => ({
275
+ code: capString(exclusion.code, budget),
276
+ ...(exclusion.detail !== undefined
277
+ ? { detail: capString(exclusion.detail, budget) }
278
+ : {}),
279
+ })),
280
+ ...(input.score ? { score: input.score } : {}),
281
+ ...(capability ? { capability } : {}),
282
+ ...(health ? { health } : {}),
283
+ ...(quota ? { quota } : {}),
284
+ ...(cost ? { cost } : {}),
285
+ ...(compatibility ? { compatibility } : {}),
286
+ };
287
+ }
288
+
289
+ /** Bounded requirement copy with capped strings. */
290
+ function buildRequirement(requirement: RouteRequirementEvidence, budget: { strings?: true }): RouteRequirementEvidence {
291
+ return {
292
+ id: capString(requirement.id, budget),
293
+ ...(requirement.expected !== undefined
294
+ ? { expected: typeof requirement.expected === "string"
295
+ ? capString(requirement.expected, budget)
296
+ : requirement.expected }
297
+ : {}),
298
+ ...(requirement.actual !== undefined
299
+ ? { actual: typeof requirement.actual === "string"
300
+ ? capString(requirement.actual, budget)
301
+ : requirement.actual }
302
+ : {}),
303
+ outcome: requirement.outcome,
304
+ };
305
+ }
306
+
307
+ /**
308
+ * Build a bounded decision trace. The builder never receives credentials: callers
309
+ * pass provider/model NAME strings and opaque account references only.
310
+ */
311
+ export function buildRouteDecisionTrace(input: TraceBuildInput): RouteDecisionTraceV1 {
312
+ const budget: ParseCaps = {};
313
+ const now = input.now ?? Date.now();
314
+ const truncated: RouteDecisionTraceV1["truncated"] = {};
315
+ let selectedIndex = Number.isInteger(input.selected.candidateIndex ?? 0)
316
+ ? (input.selected.candidateIndex ?? 0)
317
+ : 0;
318
+
319
+ let candidates = (input.candidates ?? []).map(candidate => buildCandidate(candidate, budget));
320
+ if (candidates.length > MAX_TRACE_CANDIDATES) {
321
+ candidates = selectedIndex < MAX_TRACE_CANDIDATES
322
+ ? candidates.slice(0, MAX_TRACE_CANDIDATES)
323
+ : [...candidates.slice(0, MAX_TRACE_CANDIDATES - 1), candidates[selectedIndex]!];
324
+ selectedIndex = Math.min(selectedIndex, candidates.length - 1);
325
+ truncated.candidates = true;
326
+ }
327
+ if (candidates.length === 0) {
328
+ candidates = [{
329
+ provider: capString(input.selected.provider, budget),
330
+ model: capString(input.selected.model, budget),
331
+ ...(input.selected.accountRef !== undefined
332
+ ? { accountRef: capString(input.selected.accountRef, budget) }
333
+ : {}),
334
+ eligible: true,
335
+ exclusions: [],
336
+ }];
337
+ }
338
+
339
+ let requirements = (input.requirements ?? []).map(requirement => buildRequirement(requirement, budget));
340
+ if (requirements.length > MAX_REQUIREMENTS) {
341
+ requirements = requirements.slice(0, MAX_REQUIREMENTS);
342
+ truncated.requirements = true;
343
+ }
344
+
345
+ if (selectedIndex < 0 || selectedIndex >= candidates.length) selectedIndex = 0;
346
+
347
+ const trace: RouteDecisionTraceV1 = {
348
+ version: 1,
349
+ decisionId: randomBytes(6).toString("hex"),
350
+ createdAt: now,
351
+ requestedModel: capString(input.requestedModel, budget),
352
+ routeKind: input.routeKind,
353
+ ...(input.profile
354
+ ? {
355
+ profile: {
356
+ id: capString(input.profile.id, budget),
357
+ revision: capString(input.profile.revision, budget),
358
+ },
359
+ }
360
+ : {}),
361
+ requirements,
362
+ candidates,
363
+ selected: {
364
+ candidateIndex: selectedIndex,
365
+ provider: capString(input.selected.provider, budget),
366
+ model: capString(input.selected.model, budget),
367
+ ...(input.selected.accountRef !== undefined
368
+ ? { accountRef: capString(input.selected.accountRef, budget) }
369
+ : {}),
370
+ reason: capString(input.selected.reason, budget),
371
+ ...(input.selected.tieBreak !== undefined
372
+ ? { tieBreak: capString(input.selected.tieBreak, budget) }
373
+ : {}),
374
+ },
375
+ };
376
+
377
+ if (budget.strings) truncated.strings = true;
378
+ if (budget.exclusions) truncated.exclusions = true;
379
+ if (budget.candidates) truncated.candidates = true;
380
+ if (budget.compatibility) truncated.compatibility = true;
381
+ if (Object.keys(truncated).length > 0) trace.truncated = truncated;
382
+
383
+ return enforceByteBudget(trace);
384
+ }
385
+
386
+ /** Serialized UTF-8 length of a value (JSON is measured in bytes, not code units). */
387
+ function serializedByteLength(value: unknown): number {
388
+ return Buffer.byteLength(JSON.stringify(value), "utf8");
389
+ }
390
+
391
+ /** Deterministic byte-budget enforcement: drop details, then shrink candidates. */
392
+ function enforceByteBudget(trace: RouteDecisionTraceV1): RouteDecisionTraceV1 {
393
+ if (serializedByteLength(trace) <= MAX_TRACE_BYTES) return trace;
394
+ const truncated = { ...trace.truncated, strings: true as const };
395
+ const candidates = trace.candidates.map(candidate => ({
396
+ ...candidate,
397
+ exclusions: candidate.exclusions.map(exclusion => ({ code: exclusion.code })),
398
+ }));
399
+ const slimmed: RouteDecisionTraceV1 = { ...trace, truncated, candidates };
400
+ if (serializedByteLength(slimmed) <= MAX_TRACE_BYTES) return slimmed;
401
+ const half = Math.max(1, Math.floor(MAX_TRACE_CANDIDATES / 2));
402
+ const selectedIndex = trace.selected.candidateIndex;
403
+ const kept = selectedIndex < half
404
+ ? slimmed.candidates.slice(0, half)
405
+ : [...slimmed.candidates.slice(0, half - 1), slimmed.candidates[selectedIndex]!];
406
+ let result: RouteDecisionTraceV1 = {
407
+ ...slimmed,
408
+ truncated: { ...truncated, candidates: true as const },
409
+ candidates: kept,
410
+ selected: { ...slimmed.selected, candidateIndex: Math.min(selectedIndex, kept.length - 1) },
411
+ };
412
+ for (let stage = 0; stage < 4 && serializedByteLength(result) > MAX_TRACE_BYTES; stage++) {
413
+ if (stage === 0) {
414
+ result = {
415
+ ...result,
416
+ truncated: { ...result.truncated, exclusions: true as const },
417
+ candidates: result.candidates.map(candidate => ({
418
+ ...candidate,
419
+ exclusions: candidate.exclusions.slice(0, 8),
420
+ })),
421
+ };
422
+ } else if (stage === 1) {
423
+ result = {
424
+ ...result,
425
+ truncated: { ...result.truncated, exclusions: true as const },
426
+ candidates: result.candidates.map(candidate => ({ ...candidate, exclusions: [] })),
427
+ };
428
+ } else {
429
+ const half = Math.max(1, Math.ceil(result.candidates.length / 2));
430
+ const index = result.selected.candidateIndex;
431
+ const shrinkKept = index < half
432
+ ? result.candidates.slice(0, half)
433
+ : [...result.candidates.slice(0, half - 1), result.candidates[index]!];
434
+ result = {
435
+ ...result,
436
+ truncated: { ...result.truncated, candidates: true as const },
437
+ candidates: shrinkKept,
438
+ selected: { ...result.selected, candidateIndex: Math.min(index, shrinkKept.length - 1) },
439
+ };
440
+ }
441
+ }
442
+ return result;
443
+ }
444
+
445
+ /** Defensive parse of one persisted exclusion reason. */
446
+ function parseExclusion(raw: unknown, caps: ParseCaps): RouteExclusionReason | null {
447
+ if (!isPlainRecord(raw)) return null;
448
+ const code = raw.code;
449
+ if (typeof code !== "string" || code.length === 0) return null;
450
+ if (code.length > MAX_TRACE_STRING) caps.strings = true;
451
+ const out: RouteExclusionReason = { code: code.slice(0, MAX_TRACE_STRING) };
452
+ if (typeof raw.detail === "string") {
453
+ if (raw.detail.length > MAX_TRACE_STRING) caps.strings = true;
454
+ out.detail = raw.detail.slice(0, MAX_TRACE_STRING);
455
+ }
456
+ return out;
457
+ }
458
+
459
+ /** Defensive parse of one persisted requirement; rejects unknown outcomes. */
460
+ function parseRequirement(raw: unknown, caps: ParseCaps): RouteRequirementEvidence | null {
461
+ if (!isPlainRecord(raw)) return null;
462
+ const id = raw.id;
463
+ const outcome = raw.outcome;
464
+ if (typeof id !== "string" || id.length === 0) return null;
465
+ if (typeof outcome !== "string" || !REQUIREMENT_OUTCOMES.has(outcome)) return null;
466
+ if (id.length > MAX_TRACE_STRING) caps.strings = true;
467
+ const out: RouteRequirementEvidence = {
468
+ id: id.slice(0, MAX_TRACE_STRING),
469
+ outcome: outcome as RouteRequirementEvidence["outcome"],
470
+ };
471
+ if (typeof raw.expected === "string") {
472
+ if (raw.expected.length > MAX_TRACE_STRING) caps.strings = true;
473
+ out.expected = raw.expected.slice(0, MAX_TRACE_STRING);
474
+ } else if (typeof raw.expected === "number" || typeof raw.expected === "boolean") {
475
+ out.expected = raw.expected;
476
+ }
477
+ if (typeof raw.actual === "string") {
478
+ if (raw.actual.length > MAX_TRACE_STRING) caps.strings = true;
479
+ out.actual = raw.actual.slice(0, MAX_TRACE_STRING);
480
+ } else if (unknownable(raw.actual) !== undefined) {
481
+ out.actual = unknownable(raw.actual);
482
+ }
483
+ return out;
484
+ }
485
+
486
+ /** Whitelisted capability-evidence parse; unknown fields are dropped. */
487
+ function parseCapability(raw: unknown, caps: ParseCaps): RouteCapabilityEvidence | undefined {
488
+ if (!isPlainRecord(raw)) return undefined;
489
+ const out: RouteCapabilityEvidence = {};
490
+ if (finiteNumber(raw.contextWindow)) out.contextWindow = raw.contextWindow;
491
+ const tools = unknownable(raw.tools);
492
+ if (tools !== undefined) out.tools = tools;
493
+ const image = unknownable(raw.image);
494
+ if (image !== undefined) out.image = image;
495
+ const structuredOutput = unknownable(raw.structuredOutput);
496
+ if (structuredOutput !== undefined) out.structuredOutput = structuredOutput;
497
+ const rawReasoningEfforts = Array.isArray(raw.reasoningEfforts)
498
+ ? raw.reasoningEfforts
499
+ : undefined;
500
+ const reasoningEfforts = rawReasoningEfforts
501
+ ? Array.from(
502
+ { length: Math.min(rawReasoningEfforts.length, 8) },
503
+ (_, index) => rawReasoningEfforts[index],
504
+ )
505
+ : undefined;
506
+ if (reasoningEfforts
507
+ && reasoningEfforts.every((value): value is string => typeof value === "string")) {
508
+ if (reasoningEfforts.some((value: unknown) => typeof value === "string"
509
+ && value.length > MAX_TRACE_STRING)) caps.strings = true;
510
+ out.reasoningEfforts = reasoningEfforts.map(value => value.slice(0, MAX_TRACE_STRING));
511
+ }
512
+ if (raw.serviceTier === "unknown") {
513
+ out.serviceTier = "unknown";
514
+ } else if (typeof raw.serviceTier === "string" && raw.serviceTier) {
515
+ if (raw.serviceTier.length > MAX_TRACE_STRING) caps.strings = true;
516
+ out.serviceTier = raw.serviceTier.slice(0, MAX_TRACE_STRING);
517
+ }
518
+ const localOnly = unknownable(raw.localOnly);
519
+ if (localOnly !== undefined) out.localOnly = localOnly;
520
+ const remoteAllowed = unknownable(raw.remoteAllowed);
521
+ if (remoteAllowed !== undefined) out.remoteAllowed = remoteAllowed;
522
+ const encryptedCodexTasks = unknownable(raw.encryptedCodexTasks);
523
+ if (encryptedCodexTasks !== undefined) out.encryptedCodexTasks = encryptedCodexTasks;
524
+ return Object.keys(out).length > 0 ? out : undefined;
525
+ }
526
+
527
+ /** Whitelisted health-evidence parse; non-numeric fields are dropped. */
528
+ function parseHealth(raw: unknown): RouteHealthEvidence | undefined {
529
+ if (!isPlainRecord(raw)) return undefined;
530
+ const out: RouteHealthEvidence = {};
531
+ if (finiteNumber(raw.cooldownUntilMs)) out.cooldownUntilMs = raw.cooldownUntilMs;
532
+ if (finiteNumber(raw.softAvoidUntilMs)) out.softAvoidUntilMs = raw.softAvoidUntilMs;
533
+ if (finiteNumber(raw.successRate)) out.successRate = raw.successRate;
534
+ if (finiteNumber(raw.failures)) out.failures = raw.failures;
535
+ if (finiteNumber(raw.incompleteStreamRate)) out.incompleteStreamRate = raw.incompleteStreamRate;
536
+ if (finiteNumber(raw.recentLatencyMs)) out.recentLatencyMs = raw.recentLatencyMs;
537
+ if (finiteNumber(raw.sampleCount)) out.sampleCount = raw.sampleCount;
538
+ if (finiteNumber(raw.recencyWeight)) out.recencyWeight = raw.recencyWeight;
539
+ return Object.keys(out).length > 0 ? out : undefined;
540
+ }
541
+
542
+ /** Whitelisted quota-evidence parse; requires a boolean `known`. */
543
+ function parseQuota(raw: unknown, caps: ParseCaps): RouteQuotaEvidence | undefined {
544
+ if (!isPlainRecord(raw)) return undefined;
545
+ if (typeof raw.known !== "boolean") return undefined;
546
+ const out: RouteQuotaEvidence = { known: raw.known };
547
+ if (finiteNumber(raw.headroom)) out.headroom = Math.max(0, Math.min(1, raw.headroom));
548
+ if (finiteNumber(raw.headroomTokens)) out.headroomTokens = raw.headroomTokens;
549
+ if (typeof raw.exhausted === "boolean") out.exhausted = raw.exhausted;
550
+ if (finiteNumber(raw.resetAtMs)) out.resetAtMs = raw.resetAtMs;
551
+ if (typeof raw.reauthOrCooling === "boolean") out.reauthOrCooling = raw.reauthOrCooling;
552
+ if (finiteNumber(raw.reservedHeadroomTokens)) out.reservedHeadroomTokens = raw.reservedHeadroomTokens;
553
+ if (typeof raw.source === "string" && raw.source) {
554
+ if (raw.source.length > MAX_TRACE_STRING) caps.strings = true;
555
+ out.source = raw.source.slice(0, MAX_TRACE_STRING);
556
+ }
557
+ return out;
558
+ }
559
+
560
+ const COST_CAP_OUTCOMES = new Set<RouteCostCapOutcome>([
561
+ "satisfied",
562
+ "exceeded",
563
+ "unknown-allowed",
564
+ "unknown-excluded",
565
+ ]);
566
+
567
+ /** Whitelisted cost-evidence parse. */
568
+ function parseCost(raw: unknown, caps: ParseCaps): RouteCostEvidence | undefined {
569
+ if (!isPlainRecord(raw)) return undefined;
570
+ const out: RouteCostEvidence = {};
571
+ if (finiteNumber(raw.estimatedUsd)) out.estimatedUsd = raw.estimatedUsd;
572
+ if (typeof raw.priceSource === "string" && raw.priceSource) {
573
+ if (raw.priceSource.length > MAX_TRACE_STRING) caps.strings = true;
574
+ out.priceSource = raw.priceSource.slice(0, MAX_TRACE_STRING);
575
+ }
576
+ if (typeof raw.incomplete === "boolean") out.incomplete = raw.incomplete;
577
+ if (finiteNumber(raw.limitUsd)) out.limitUsd = raw.limitUsd;
578
+ if (out.limitUsd !== undefined
579
+ && typeof raw.capOutcome === "string"
580
+ && COST_CAP_OUTCOMES.has(raw.capOutcome as RouteCostCapOutcome)) {
581
+ out.capOutcome = raw.capOutcome as RouteCostCapOutcome;
582
+ }
583
+ return Object.keys(out).length > 0 ? out : undefined;
584
+ }
585
+
586
+ const MAX_COMPATIBILITY_SUITES = 8;
587
+ const COMPATIBILITY_OUTCOMES = new Set(["satisfied", "penalized", "excluded", "unknown"]);
588
+
589
+ function parseCompatibility(raw: unknown, caps: ParseCaps): RouteCompatibilityEvidence | undefined {
590
+ if (!isPlainRecord(raw)) return undefined;
591
+ const suitesRaw = Array.isArray(raw.suites) ? raw.suites : [];
592
+ if (suitesRaw.length > MAX_COMPATIBILITY_SUITES || raw.truncated === true) caps.compatibility = true;
593
+ const suites: RouteCompatibilitySuiteTrace[] = [];
594
+ for (const entry of suitesRaw.slice(0, MAX_COMPATIBILITY_SUITES)) {
595
+ if (!isPlainRecord(entry)) continue;
596
+ const suiteId = entry.suiteId;
597
+ const evidenceLayer = entry.evidenceLayer;
598
+ const outcome = entry.outcome;
599
+ if (typeof suiteId !== "string" || typeof evidenceLayer !== "string") continue;
600
+ if (typeof outcome !== "string" || !COMPATIBILITY_OUTCOMES.has(outcome)) continue;
601
+ const row: RouteCompatibilitySuiteTrace = {
602
+ suiteId: capString(suiteId, caps),
603
+ evidenceLayer: capString(evidenceLayer, caps),
604
+ outcome: outcome as RouteCompatibilitySuiteTrace["outcome"],
605
+ };
606
+ if (typeof entry.subjectIdPrefix === "string" && entry.subjectIdPrefix) {
607
+ row.subjectIdPrefix = capString(entry.subjectIdPrefix, caps);
608
+ }
609
+ for (const key of ["verdict", "minStatus", "reason", "unknownPolicy", "degradedPolicy"] as const) {
610
+ if (typeof entry[key] === "string") row[key] = capString(entry[key] as string, caps);
611
+ }
612
+ if (typeof entry.fresh === "boolean") row.fresh = entry.fresh;
613
+ suites.push(row);
614
+ }
615
+ const out: RouteCompatibilityEvidence = {
616
+ suites,
617
+ ...(raw.truncated === true ? { truncated: true as const } : {}),
618
+ };
619
+ if (typeof raw.subjectIdPrefix === "string" && raw.subjectIdPrefix) {
620
+ out.subjectIdPrefix = capString(raw.subjectIdPrefix, caps);
621
+ }
622
+ return suites.length > 0 || out.subjectIdPrefix || out.truncated ? out : undefined;
623
+ }
624
+
625
+ /** Whitelisted score parse; requires a finite `total` and bounded components. */
626
+ function parseScore(raw: unknown): RouteScoreEvidence | undefined {
627
+ if (!isPlainRecord(raw)) return undefined;
628
+ if (!finiteNumber(raw.total)) return undefined;
629
+ const components = isPlainRecord(raw.components) ? raw.components : {};
630
+ const parsedComponents: RouteScoreEvidence["components"] = {};
631
+ for (const key of ["capability", "health", "quota", "cost", "latency", "configuredPriority", "compatibility"] as const) {
632
+ if (finiteNumber(components[key])) parsedComponents[key] = components[key];
633
+ }
634
+ return { total: raw.total, components: parsedComponents };
635
+ }
636
+
637
+ /** Defensive parse of one persisted candidate with bounded evidence blocks. */
638
+ function parseCandidate(raw: unknown, caps: ParseCaps): RouteCandidateTrace | null {
639
+ if (!isPlainRecord(raw)) return null;
640
+ const provider = raw.provider;
641
+ const model = raw.model;
642
+ if (typeof provider !== "string" || provider.length === 0) return null;
643
+ if (typeof model !== "string" || model.length === 0) return null;
644
+ if (provider.length > MAX_TRACE_STRING) caps.strings = true;
645
+ if (model.length > MAX_TRACE_STRING) caps.strings = true;
646
+ if (typeof raw.accountRef === "string" && raw.accountRef.length > MAX_TRACE_STRING) caps.strings = true;
647
+ if (typeof raw.eligible !== "boolean") return null;
648
+ if (Array.isArray(raw.exclusions) && raw.exclusions.length > MAX_EXCLUSIONS_PER_CANDIDATE) caps.exclusions = true;
649
+ const exclusions = Array.isArray(raw.exclusions)
650
+ ? raw.exclusions.slice(0, MAX_EXCLUSIONS_PER_CANDIDATE)
651
+ .map(value => parseExclusion(value, caps))
652
+ .filter((value): value is RouteExclusionReason => value !== null)
653
+ : [];
654
+ const capability = parseCapability(raw.capability, caps);
655
+ const health = parseHealth(raw.health);
656
+ const quota = parseQuota(raw.quota, caps);
657
+ const cost = parseCost(raw.cost, caps);
658
+ const compatibility = parseCompatibility(raw.compatibility, caps);
659
+ const score = parseScore(raw.score);
660
+ return {
661
+ provider: provider.slice(0, MAX_TRACE_STRING),
662
+ model: model.slice(0, MAX_TRACE_STRING),
663
+ ...(typeof raw.accountRef === "string"
664
+ ? { accountRef: raw.accountRef.slice(0, MAX_TRACE_STRING) }
665
+ : {}),
666
+ eligible: raw.eligible,
667
+ exclusions: exclusions.slice(0, MAX_EXCLUSIONS_PER_CANDIDATE),
668
+ ...(capability ? { capability } : {}),
669
+ ...(health ? { health } : {}),
670
+ ...(quota ? { quota } : {}),
671
+ ...(cost ? { cost } : {}),
672
+ ...(compatibility ? { compatibility } : {}),
673
+ ...(score ? { score } : {}),
674
+ };
675
+ }
676
+
677
+ /**
678
+ * Defensive parse of a persisted trace. Returns null when the row is not a
679
+ * version-1 trace; otherwise returns a bounded, whitelisted copy. Invalid
680
+ * evidence objects are dropped rather than poisoning the DTO.
681
+ */
682
+ export function normalizeRouteDecisionTrace(raw: unknown): RouteDecisionTraceV1 | null {
683
+ if (!isPlainRecord(raw)) return null;
684
+ if (raw.version !== 1) return null;
685
+ const caps: ParseCaps = {};
686
+ const decisionId = raw.decisionId;
687
+ const createdAt = raw.createdAt;
688
+ const requestedModel = raw.requestedModel;
689
+ const routeKind = raw.routeKind;
690
+ if (typeof decisionId !== "string" || decisionId.length === 0) return null;
691
+ if (!/^[0-9a-f]{12}$/.test(decisionId)) return null;
692
+ if (decisionId.length > MAX_TRACE_STRING) caps.strings = true;
693
+ if (!finiteNumber(createdAt)) return null;
694
+ if (typeof requestedModel !== "string" || requestedModel.length === 0) return null;
695
+ if (requestedModel.length > MAX_TRACE_STRING) caps.strings = true;
696
+ if (typeof routeKind !== "string" || !ROUTE_KINDS.has(routeKind as RouteDecisionKind)) return null;
697
+ if (!Array.isArray(raw.candidates) || raw.candidates.length === 0) return null;
698
+ if (raw.candidates.length > MAX_TRACE_CANDIDATES) caps.candidates = true;
699
+
700
+ const candidates = raw.candidates
701
+ .slice(0, MAX_TRACE_CANDIDATES)
702
+ .map(value => parseCandidate(value, caps))
703
+ .filter((value): value is RouteCandidateTrace => value !== null)
704
+ .slice(0, MAX_TRACE_CANDIDATES);
705
+ if (candidates.length === 0) return null;
706
+
707
+ const selected = isPlainRecord(raw.selected) ? raw.selected : null;
708
+ if (!selected) return null;
709
+ const selectedProvider = selected.provider;
710
+ const selectedModel = selected.model;
711
+ const selectedReason = selected.reason;
712
+ if (typeof selectedProvider !== "string" || typeof selectedModel !== "string") return null;
713
+ if (typeof selectedReason !== "string") return null;
714
+ if (selectedProvider.length > MAX_TRACE_STRING) caps.strings = true;
715
+ if (selectedModel.length > MAX_TRACE_STRING) caps.strings = true;
716
+ if (selectedReason.length > MAX_TRACE_STRING) caps.strings = true;
717
+ if (typeof selected.accountRef === "string" && selected.accountRef.length > MAX_TRACE_STRING) caps.strings = true;
718
+ if (typeof selected.tieBreak === "string" && selected.tieBreak.length > MAX_TRACE_STRING) caps.strings = true;
719
+ const candidateIndex = Number.isInteger(selected.candidateIndex)
720
+ ? selected.candidateIndex as number
721
+ : 0;
722
+ if (candidateIndex < 0 || candidateIndex >= candidates.length) return null;
723
+
724
+ const rawRequirements = Array.isArray(raw.requirements) ? raw.requirements : [];
725
+ if (rawRequirements.length > MAX_REQUIREMENTS) caps.requirements = true;
726
+ const requirements = rawRequirements
727
+ .slice(0, MAX_REQUIREMENTS)
728
+ .map(value => parseRequirement(value, caps))
729
+ .filter((value): value is RouteRequirementEvidence => value !== null)
730
+ .slice(0, MAX_REQUIREMENTS);
731
+
732
+ const profile = isPlainRecord(raw.profile)
733
+ && typeof raw.profile.id === "string"
734
+ && typeof raw.profile.revision === "string"
735
+ ? (() => {
736
+ if (raw.profile.id.length > MAX_TRACE_STRING) caps.strings = true;
737
+ if (raw.profile.revision.length > MAX_TRACE_STRING) caps.strings = true;
738
+ return {
739
+ id: raw.profile.id.slice(0, MAX_TRACE_STRING),
740
+ revision: raw.profile.revision.slice(0, MAX_TRACE_STRING),
741
+ };
742
+ })()
743
+ : undefined;
744
+
745
+ const incoming = isPlainRecord(raw.truncated) ? raw.truncated : {};
746
+ const truncated: RouteDecisionTraceV1["truncated"] = {};
747
+ if (incoming.candidates === true || caps.candidates) truncated.candidates = true;
748
+ if (incoming.exclusions === true || caps.exclusions) truncated.exclusions = true;
749
+ if (incoming.requirements === true || caps.requirements) truncated.requirements = true;
750
+ if (incoming.strings === true || caps.strings) truncated.strings = true;
751
+ if (incoming.compatibility === true || caps.compatibility) truncated.compatibility = true;
752
+
753
+ return {
754
+ version: 1,
755
+ decisionId: decisionId.slice(0, MAX_TRACE_STRING),
756
+ createdAt,
757
+ requestedModel: requestedModel.slice(0, MAX_TRACE_STRING),
758
+ routeKind: routeKind as RouteDecisionKind,
759
+ ...(profile ? { profile } : {}),
760
+ requirements,
761
+ candidates,
762
+ selected: {
763
+ candidateIndex,
764
+ provider: selectedProvider.slice(0, MAX_TRACE_STRING),
765
+ model: selectedModel.slice(0, MAX_TRACE_STRING),
766
+ ...(typeof selected.accountRef === "string"
767
+ ? { accountRef: selected.accountRef.slice(0, MAX_TRACE_STRING) }
768
+ : {}),
769
+ reason: selectedReason.slice(0, MAX_TRACE_STRING),
770
+ ...(typeof selected.tieBreak === "string"
771
+ ? { tieBreak: selected.tieBreak.slice(0, MAX_TRACE_STRING) }
772
+ : {}),
773
+ },
774
+ ...(Object.keys(truncated).length > 0 ? { truncated } : {}),
775
+ };
776
+ }