@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,547 @@
1
+ /**
2
+ * Routing policy profiles (RI-04): schema validation, normalization, revision
3
+ * digest, and id/alias resolution. Mirrors the combos module discipline
4
+ * (`src/combos/types.ts`) so both virtual-routing namespaces stay consistent.
5
+ */
6
+
7
+ import { createHash } from "node:crypto";
8
+ import type {
9
+ OcxConfig,
10
+ OcxRoutingProfileConfig,
11
+ OcxRoutingUnknownEvidenceMode,
12
+ OcxRoutingUnknownCostCapMode,
13
+ } from "../types";
14
+ import { codexAccountNamespaceEntries } from "../codex/account-namespaces";
15
+ import { listComboIds, resolveComboId } from "../combos";
16
+ import { hasOwnProvider } from "../config/provider-name";
17
+ import { MAX_COMPATIBILITY_REQUIRED_SUITES } from "./compatibility/types";
18
+ import { POLICY_NAMESPACE } from "./profile-namespace";
19
+
20
+ export { POLICY_NAMESPACE };
21
+
22
+ export const POLICY_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
23
+ export const POLICY_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}(?:\/[A-Za-z0-9][A-Za-z0-9._-]{0,63})?$/;
24
+ export const NATIVE_OPENAI_FAMILY_PATTERN = /^(?:gpt-|o1-|o3-|o4-|codex-)/;
25
+
26
+ export const DEFAULT_PROFILE_WEIGHTS = {
27
+ latency: 0.55,
28
+ health: 0.25,
29
+ cost: 0.10,
30
+ quota: 0.10,
31
+ } as const;
32
+
33
+ export const DEFAULT_UNKNOWN_EVIDENCE: Record<"capability" | "health" | "quota" | "cost", OcxRoutingUnknownEvidenceMode> = {
34
+ capability: "exclude",
35
+ health: "penalize",
36
+ quota: "penalize",
37
+ cost: "penalize",
38
+ };
39
+
40
+ export const DEFAULT_DEGRADED_EVIDENCE: OcxRoutingUnknownEvidenceMode = "penalize";
41
+ export const DEFAULT_COMPATIBILITY_UNKNOWN_EVIDENCE: OcxRoutingUnknownEvidenceMode = "exclude";
42
+
43
+ export interface NormalizedRoutingProfileCompatibility {
44
+ requiredSuites: Array<{ suiteId: string; evidenceLayer: "protocol_conformance" | "live_route_compatibility" }>;
45
+ minStatus?: "PROBED" | "VERIFIED";
46
+ maxEvidenceAgeMs?: number;
47
+ unknownEvidence: OcxRoutingUnknownEvidenceMode;
48
+ degradedEvidence: OcxRoutingUnknownEvidenceMode;
49
+ }
50
+
51
+ export interface RoutingProfileValidationIssue {
52
+ path: Array<string | number>;
53
+ message: string;
54
+ }
55
+
56
+ export interface NormalizedRoutingProfileRequirements {
57
+ minContextWindow?: number;
58
+ /** Minimum remaining quota headroom fraction (0..1). */
59
+ minQuotaHeadroom?: number;
60
+ tools?: boolean;
61
+ imageInput?: boolean;
62
+ structuredOutput?: boolean;
63
+ reasoningEffort?: string;
64
+ serviceTier?: string;
65
+ localOnly?: boolean;
66
+ remoteAllowed?: boolean;
67
+ encryptedCodexTasks?: boolean;
68
+ }
69
+
70
+ export interface NormalizedRoutingProfile {
71
+ id: string;
72
+ alias: string | null;
73
+ candidates: Array<{ provider: string; model: string }>;
74
+ require: NormalizedRoutingProfileRequirements;
75
+ optimize: { latency: number; health: number; cost: number; quota: number };
76
+ limits: { maxEstimatedCostUsd?: number; onUnknownCost?: OcxRoutingUnknownCostCapMode };
77
+ unknownEvidence: Record<"capability" | "health" | "quota" | "cost", OcxRoutingUnknownEvidenceMode>;
78
+ compatibility?: NormalizedRoutingProfileCompatibility;
79
+ revision: string;
80
+ }
81
+
82
+ const REQUIRE_KEYS = [
83
+ "minContextWindow",
84
+ "minQuotaHeadroom",
85
+ "tools",
86
+ "imageInput",
87
+ "structuredOutput",
88
+ "reasoningEffort",
89
+ "serviceTier",
90
+ "localOnly",
91
+ "remoteAllowed",
92
+ "encryptedCodexTasks",
93
+ ] as const;
94
+
95
+ const UNKNOWN_EVIDENCE_KEYS = ["capability", "health", "quota", "cost"] as const;
96
+ const EVIDENCE_LAYERS = new Set(["protocol_conformance", "live_route_compatibility"]);
97
+
98
+ export function isValidPolicyId(id: string): boolean {
99
+ return POLICY_ID_PATTERN.test(id);
100
+ }
101
+
102
+ export function policyModelId(id: string): string {
103
+ return `${POLICY_NAMESPACE}/${id}`;
104
+ }
105
+
106
+ export function policyPublicModelId(id: string, profile: { alias?: string | null }): string {
107
+ const alias = typeof profile.alias === "string" ? profile.alias.trim() : "";
108
+ return alias || policyModelId(id);
109
+ }
110
+
111
+ export function parsePolicyModelId(modelId: string): string | null {
112
+ const slash = modelId.indexOf("/");
113
+ if (slash <= 0 || modelId.slice(0, slash) !== POLICY_NAMESPACE) return null;
114
+ const id = modelId.slice(slash + 1);
115
+ return id.length > 0 ? id : null;
116
+ }
117
+
118
+ /**
119
+ * Resolve a client-requested model id to a policy profile id. The canonical
120
+ * `policy/<id>` form wins first; otherwise an exact alias match.
121
+ */
122
+ export function resolvePolicyProfileId(
123
+ config: { routingProfiles?: Record<string, OcxRoutingProfileConfig> },
124
+ modelId: string,
125
+ ): string | null {
126
+ const direct = parsePolicyModelId(modelId);
127
+ if (direct) return direct;
128
+ const profiles = config.routingProfiles;
129
+ if (!profiles) return null;
130
+ for (const [id, raw] of Object.entries(profiles)) {
131
+ if (!raw || typeof raw !== "object") continue;
132
+ const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
133
+ if (alias && alias === modelId) return id;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ function aliasIssues(
139
+ id: string,
140
+ alias: string,
141
+ config: Pick<OcxConfig, "providers" | "combos" | "routingProfiles" | "codexAccountNamespaces">,
142
+ options: { excludeProfileId?: string } = {},
143
+ ): RoutingProfileValidationIssue[] {
144
+ const issues: RoutingProfileValidationIssue[] = [];
145
+ if (!POLICY_ALIAS_PATTERN.test(alias)) {
146
+ issues.push({
147
+ path: ["alias"],
148
+ message: "alias must use letters, numbers, dot, underscore, or hyphen, with at most one \"/\" segment",
149
+ });
150
+ return issues;
151
+ }
152
+ if (alias === POLICY_NAMESPACE || alias.startsWith(`${POLICY_NAMESPACE}/`)) {
153
+ issues.push({
154
+ path: ["alias"],
155
+ message: `alias must not use the reserved "${POLICY_NAMESPACE}/" namespace`,
156
+ });
157
+ }
158
+ if (alias === "combo" || alias.startsWith("combo/")) {
159
+ issues.push({
160
+ path: ["alias"],
161
+ message: `alias must not use the reserved "combo/" namespace`,
162
+ });
163
+ }
164
+ if (!alias.includes("/") && NATIVE_OPENAI_FAMILY_PATTERN.test(alias)) {
165
+ issues.push({
166
+ path: ["alias"],
167
+ message: "bare aliases in the OpenAI native family (gpt-*, o1-*, o3-*, o4-*, codex-*) are not allowed",
168
+ });
169
+ }
170
+ if (hasOwnProvider(config.providers, alias)) {
171
+ issues.push({ path: ["alias"], message: `alias "${alias}" collides with configured provider name "${alias}"` });
172
+ }
173
+ if (resolveComboId({ combos: config.combos }, alias)) {
174
+ issues.push({ path: ["alias"], message: `alias "${alias}" collides with a configured combo selector` });
175
+ }
176
+ if (alias.includes("/") && codexAccountNamespaceEntries(config).some(([namespace]) => namespace === alias.split("/")[0])) {
177
+ issues.push({ path: ["alias"], message: `alias "${alias}" collides with a configured codex account namespace` });
178
+ }
179
+ if (alias.includes("/") && hasOwnProvider(config.providers, alias.split("/")[0])) {
180
+ issues.push({
181
+ path: ["alias"],
182
+ message: `alias "${alias}" collides with the provider routing namespace "${alias.split("/")[0]}"`,
183
+ });
184
+ }
185
+ for (const [otherId, other] of Object.entries(config.routingProfiles ?? {})) {
186
+ if (otherId === id || otherId === options.excludeProfileId) continue;
187
+ const otherAlias = typeof other?.alias === "string" ? other.alias.trim() : "";
188
+ if (otherAlias && otherAlias === alias) {
189
+ issues.push({ path: ["alias"], message: `alias "${alias}" is already used by profile "${otherId}"` });
190
+ }
191
+ }
192
+ return issues;
193
+ }
194
+
195
+ export function routingProfileIssues(
196
+ id: string,
197
+ raw: unknown,
198
+ config: Pick<OcxConfig, "providers" | "combos" | "routingProfiles" | "codexAccountNamespaces">,
199
+ options: { excludeProfileId?: string } = {},
200
+ ): RoutingProfileValidationIssue[] {
201
+ const issues: RoutingProfileValidationIssue[] = [];
202
+ if (!isValidPolicyId(id)) {
203
+ issues.push({
204
+ path: [],
205
+ message: "profile id must start with a letter/number and use letters, numbers, dot, underscore, or hyphen (max 64)",
206
+ });
207
+ }
208
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
209
+ issues.push({ path: [], message: "routing profile must be an object" });
210
+ return issues;
211
+ }
212
+ const body = raw as Record<string, unknown>;
213
+
214
+ if (body.alias !== undefined) {
215
+ if (typeof body.alias !== "string") {
216
+ issues.push({ path: ["alias"], message: "alias must be a string" });
217
+ } else {
218
+ const alias = body.alias.trim();
219
+ if (alias) issues.push(...aliasIssues(id, alias, config, options));
220
+ }
221
+ }
222
+
223
+ if (!Array.isArray(body.candidates) || body.candidates.length === 0) {
224
+ issues.push({ path: ["candidates"], message: "candidates must be a non-empty array" });
225
+ } else {
226
+ const seen = new Set<string>();
227
+ body.candidates.forEach((rawCandidate, index) => {
228
+ if (!rawCandidate || typeof rawCandidate !== "object" || Array.isArray(rawCandidate)) {
229
+ issues.push({ path: ["candidates", index], message: `candidates[${index}] must be an object` });
230
+ return;
231
+ }
232
+ const candidate = rawCandidate as Record<string, unknown>;
233
+ const provider = typeof candidate.provider === "string" ? candidate.provider.trim() : "";
234
+ const model = typeof candidate.model === "string" ? candidate.model.trim() : "";
235
+ if (!provider) {
236
+ issues.push({ path: ["candidates", index, "provider"], message: `candidates[${index}].provider is required` });
237
+ } else if (!hasOwnProvider(config.providers, provider)) {
238
+ issues.push({
239
+ path: ["candidates", index, "provider"],
240
+ message: `candidates[${index}].provider "${provider}" is not configured`,
241
+ });
242
+ } else if (config.providers[provider]?.disabled === true) {
243
+ issues.push({
244
+ path: ["candidates", index, "provider"],
245
+ message: `candidates[${index}].provider "${provider}" is disabled`,
246
+ });
247
+ }
248
+ if (!model) {
249
+ issues.push({ path: ["candidates", index, "model"], message: `candidates[${index}].model is required` });
250
+ }
251
+ if (provider && model) {
252
+ const key = `${provider}/${model}`;
253
+ if (seen.has(key)) {
254
+ issues.push({ path: ["candidates", index], message: `duplicate policy candidate "${key}"` });
255
+ } else {
256
+ seen.add(key);
257
+ }
258
+ }
259
+ });
260
+ }
261
+
262
+ if (body.require !== undefined) {
263
+ if (!body.require || typeof body.require !== "object" || Array.isArray(body.require)) {
264
+ issues.push({ path: ["require"], message: "require must be an object" });
265
+ } else {
266
+ const require = body.require as Record<string, unknown>;
267
+ if (require.minContextWindow !== undefined
268
+ && (typeof require.minContextWindow !== "number"
269
+ || !Number.isInteger(require.minContextWindow)
270
+ || require.minContextWindow < 1)) {
271
+ issues.push({ path: ["require", "minContextWindow"], message: "minContextWindow must be a positive integer" });
272
+ }
273
+ if (require.minQuotaHeadroom !== undefined
274
+ && (typeof require.minQuotaHeadroom !== "number"
275
+ || !Number.isFinite(require.minQuotaHeadroom)
276
+ || require.minQuotaHeadroom < 0
277
+ || require.minQuotaHeadroom > 1)) {
278
+ issues.push({ path: ["require", "minQuotaHeadroom"], message: "minQuotaHeadroom must be a number from 0 to 1" });
279
+ }
280
+ for (const key of ["tools", "imageInput", "structuredOutput", "localOnly", "remoteAllowed", "encryptedCodexTasks"] as const) {
281
+ if (require[key] !== undefined && typeof require[key] !== "boolean") {
282
+ issues.push({ path: ["require", key], message: `${key} must be a boolean` });
283
+ }
284
+ }
285
+ if (require.reasoningEffort !== undefined && typeof require.reasoningEffort !== "string") {
286
+ issues.push({ path: ["require", "reasoningEffort"], message: "reasoningEffort must be a string" });
287
+ }
288
+ if (require.serviceTier !== undefined && typeof require.serviceTier !== "string") {
289
+ issues.push({ path: ["require", "serviceTier"], message: "serviceTier must be a string" });
290
+ } else if (require.serviceTier === "unknown") {
291
+ issues.push({
292
+ path: ["require", "serviceTier"],
293
+ message: "serviceTier must not use the reserved \"unknown\" value (it encodes missing evidence)",
294
+ });
295
+ }
296
+ }
297
+ }
298
+
299
+ if (body.optimize !== undefined) {
300
+ if (!body.optimize || typeof body.optimize !== "object" || Array.isArray(body.optimize)) {
301
+ issues.push({ path: ["optimize"], message: "optimize must be an object" });
302
+ } else {
303
+ const optimize = body.optimize as Record<string, unknown>;
304
+ const effective: Record<"latency" | "health" | "cost" | "quota", number> = { ...DEFAULT_PROFILE_WEIGHTS };
305
+ for (const key of ["latency", "health", "cost", "quota"] as const) {
306
+ if (optimize[key] !== undefined) {
307
+ if (typeof optimize[key] !== "number"
308
+ || !Number.isFinite(optimize[key])
309
+ || optimize[key] < 0) {
310
+ issues.push({ path: ["optimize", key], message: `${key} must be a non-negative number` });
311
+ } else {
312
+ effective[key] = optimize[key];
313
+ }
314
+ }
315
+ }
316
+ if (Object.values(effective).every(weight => weight === 0)) {
317
+ issues.push({ path: ["optimize"], message: "at least one optimize weight must be positive" });
318
+ }
319
+ }
320
+ }
321
+
322
+ if (body.limits !== undefined) {
323
+ if (!body.limits || typeof body.limits !== "object" || Array.isArray(body.limits)) {
324
+ issues.push({ path: ["limits"], message: "limits must be an object" });
325
+ } else {
326
+ const limits = body.limits as Record<string, unknown>;
327
+ if (limits.maxEstimatedCostUsd !== undefined
328
+ && (typeof limits.maxEstimatedCostUsd !== "number"
329
+ || !Number.isFinite(limits.maxEstimatedCostUsd)
330
+ || limits.maxEstimatedCostUsd < 0)) {
331
+ issues.push({ path: ["limits", "maxEstimatedCostUsd"], message: "maxEstimatedCostUsd must be a non-negative number" });
332
+ }
333
+ if (limits.onUnknownCost !== undefined
334
+ && limits.onUnknownCost !== "allow"
335
+ && limits.onUnknownCost !== "exclude") {
336
+ issues.push({ path: ["limits", "onUnknownCost"], message: 'onUnknownCost must be "allow" or "exclude"' });
337
+ }
338
+ }
339
+ }
340
+
341
+ if (body.unknownEvidence !== undefined) {
342
+ if (!body.unknownEvidence || typeof body.unknownEvidence !== "object" || Array.isArray(body.unknownEvidence)) {
343
+ issues.push({ path: ["unknownEvidence"], message: "unknownEvidence must be an object" });
344
+ } else {
345
+ const unknownEvidence = body.unknownEvidence as Record<string, unknown>;
346
+ for (const key of UNKNOWN_EVIDENCE_KEYS) {
347
+ if (unknownEvidence[key] !== undefined
348
+ && (unknownEvidence[key] !== "allow"
349
+ && unknownEvidence[key] !== "penalize"
350
+ && unknownEvidence[key] !== "exclude")) {
351
+ issues.push({ path: ["unknownEvidence", key], message: `${key} must be "allow", "penalize", or "exclude"` });
352
+ }
353
+ }
354
+ }
355
+ }
356
+
357
+ if (body.compatibility !== undefined) {
358
+ if (!body.compatibility || typeof body.compatibility !== "object" || Array.isArray(body.compatibility)) {
359
+ issues.push({ path: ["compatibility"], message: "compatibility must be an object" });
360
+ } else {
361
+ const compatibility = body.compatibility as Record<string, unknown>;
362
+ if (compatibility.requiredSuites !== undefined) {
363
+ if (!Array.isArray(compatibility.requiredSuites)) {
364
+ issues.push({ path: ["compatibility", "requiredSuites"], message: "requiredSuites must be an array" });
365
+ } else {
366
+ if (compatibility.requiredSuites.length > MAX_COMPATIBILITY_REQUIRED_SUITES) {
367
+ issues.push({
368
+ path: ["compatibility", "requiredSuites"],
369
+ message: `requiredSuites must contain at most ${MAX_COMPATIBILITY_REQUIRED_SUITES} entries`,
370
+ });
371
+ }
372
+ const seen = new Set<string>();
373
+ compatibility.requiredSuites.forEach((rawSuite, index) => {
374
+ if (!rawSuite || typeof rawSuite !== "object" || Array.isArray(rawSuite)) {
375
+ issues.push({ path: ["compatibility", "requiredSuites", index], message: "suite entry must be an object" });
376
+ return;
377
+ }
378
+ const suite = rawSuite as Record<string, unknown>;
379
+ const suiteId = typeof suite.suiteId === "string" ? suite.suiteId.trim() : "";
380
+ const layer = suite.evidenceLayer;
381
+ if (!suiteId) {
382
+ issues.push({ path: ["compatibility", "requiredSuites", index, "suiteId"], message: "suiteId is required" });
383
+ }
384
+ if (layer !== "protocol_conformance" && layer !== "live_route_compatibility") {
385
+ issues.push({
386
+ path: ["compatibility", "requiredSuites", index, "evidenceLayer"],
387
+ message: "evidenceLayer must be protocol_conformance or live_route_compatibility",
388
+ });
389
+ }
390
+ if (suiteId && typeof layer === "string" && EVIDENCE_LAYERS.has(layer)) {
391
+ const key = `${layer}:${suiteId}`;
392
+ if (seen.has(key)) {
393
+ issues.push({ path: ["compatibility", "requiredSuites", index], message: `duplicate required suite "${key}"` });
394
+ } else {
395
+ seen.add(key);
396
+ }
397
+ }
398
+ });
399
+ }
400
+ }
401
+ if (compatibility.minStatus !== undefined
402
+ && compatibility.minStatus !== "PROBED"
403
+ && compatibility.minStatus !== "VERIFIED") {
404
+ issues.push({ path: ["compatibility", "minStatus"], message: 'minStatus must be "PROBED" or "VERIFIED"' });
405
+ }
406
+ if (compatibility.maxEvidenceAgeMs !== undefined
407
+ && (typeof compatibility.maxEvidenceAgeMs !== "number"
408
+ || !Number.isInteger(compatibility.maxEvidenceAgeMs)
409
+ || compatibility.maxEvidenceAgeMs < 0)) {
410
+ issues.push({ path: ["compatibility", "maxEvidenceAgeMs"], message: "maxEvidenceAgeMs must be a non-negative integer" });
411
+ }
412
+ for (const key of ["unknownEvidence", "degradedEvidence"] as const) {
413
+ if (compatibility[key] !== undefined
414
+ && compatibility[key] !== "allow"
415
+ && compatibility[key] !== "penalize"
416
+ && compatibility[key] !== "exclude") {
417
+ issues.push({ path: ["compatibility", key], message: `${key} must be "allow", "penalize", or "exclude"` });
418
+ }
419
+ }
420
+ }
421
+ }
422
+
423
+ return issues;
424
+ }
425
+
426
+ function normalizedRequirements(raw: OcxRoutingProfileConfig): NormalizedRoutingProfileRequirements {
427
+ const require = raw.require;
428
+ if (!require) return {};
429
+ const out: NormalizedRoutingProfileRequirements = {};
430
+ for (const key of REQUIRE_KEYS) {
431
+ const value = require[key];
432
+ if (value !== undefined) {
433
+ (out as Record<string, unknown>)[key] = value;
434
+ }
435
+ }
436
+ return out;
437
+ }
438
+
439
+ function normalizedUnknownEvidence(raw: OcxRoutingProfileConfig): NormalizedRoutingProfile["unknownEvidence"] {
440
+ const configured = raw.unknownEvidence;
441
+ const out = { ...DEFAULT_UNKNOWN_EVIDENCE };
442
+ if (configured) {
443
+ for (const key of UNKNOWN_EVIDENCE_KEYS) {
444
+ const value = configured[key];
445
+ if (value === "allow" || value === "penalize" || value === "exclude") {
446
+ out[key] = value;
447
+ }
448
+ }
449
+ }
450
+ return out;
451
+ }
452
+
453
+ function normalizedCompatibility(
454
+ raw: OcxRoutingProfileConfig,
455
+ ): NormalizedRoutingProfileCompatibility | undefined {
456
+ if (raw.compatibility === undefined) return undefined;
457
+ const compatibility = raw.compatibility;
458
+ const requiredSuites = (compatibility.requiredSuites ?? []).map(suite => ({
459
+ suiteId: suite.suiteId.trim(),
460
+ evidenceLayer: suite.evidenceLayer,
461
+ }));
462
+ const unknownEvidence = compatibility.unknownEvidence === "allow"
463
+ || compatibility.unknownEvidence === "penalize"
464
+ || compatibility.unknownEvidence === "exclude"
465
+ ? compatibility.unknownEvidence
466
+ : DEFAULT_COMPATIBILITY_UNKNOWN_EVIDENCE;
467
+ const degradedEvidence = compatibility.degradedEvidence === "allow"
468
+ || compatibility.degradedEvidence === "penalize"
469
+ || compatibility.degradedEvidence === "exclude"
470
+ ? compatibility.degradedEvidence
471
+ : DEFAULT_DEGRADED_EVIDENCE;
472
+
473
+ const hasControls = requiredSuites.length > 0
474
+ || compatibility.minStatus !== undefined
475
+ || compatibility.maxEvidenceAgeMs !== undefined
476
+ || compatibility.unknownEvidence !== undefined
477
+ || compatibility.degradedEvidence !== undefined;
478
+ if (!hasControls) return undefined;
479
+
480
+ return {
481
+ requiredSuites,
482
+ ...(compatibility.minStatus ? { minStatus: compatibility.minStatus } : {}),
483
+ ...(compatibility.maxEvidenceAgeMs !== undefined ? { maxEvidenceAgeMs: compatibility.maxEvidenceAgeMs } : {}),
484
+ unknownEvidence,
485
+ degradedEvidence,
486
+ };
487
+ }
488
+
489
+ function canonicalJson(value: unknown): string {
490
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
491
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
492
+ const record = value as Record<string, unknown>;
493
+ const keys = Object.keys(record).sort();
494
+ return `{${keys.map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
495
+ }
496
+
497
+ function profileRevision(profile: Omit<NormalizedRoutingProfile, "revision">): string {
498
+ const digest = createHash("sha256").update(canonicalJson(profile)).digest("hex");
499
+ return digest.slice(0, 16);
500
+ }
501
+
502
+ export function normalizeRoutingProfile(id: string, raw: OcxRoutingProfileConfig): NormalizedRoutingProfile {
503
+ const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
504
+ const weights = { ...DEFAULT_PROFILE_WEIGHTS, ...(raw.optimize ?? {}) };
505
+ const weightSum = weights.latency + weights.health + weights.cost + weights.quota;
506
+ const safeSum = weightSum > 0 ? weightSum : 1;
507
+ const compatibility = normalizedCompatibility(raw);
508
+ const profile: Omit<NormalizedRoutingProfile, "revision"> = {
509
+ id,
510
+ alias: alias || null,
511
+ candidates: raw.candidates.map(candidate => ({
512
+ provider: candidate.provider.trim(),
513
+ model: candidate.model.trim(),
514
+ })),
515
+ require: normalizedRequirements(raw),
516
+ optimize: {
517
+ latency: weights.latency / safeSum,
518
+ health: weights.health / safeSum,
519
+ cost: weights.cost / safeSum,
520
+ quota: weights.quota / safeSum,
521
+ },
522
+ limits: {
523
+ ...(raw.limits?.maxEstimatedCostUsd !== undefined
524
+ ? { maxEstimatedCostUsd: raw.limits.maxEstimatedCostUsd }
525
+ : {}),
526
+ ...(raw.limits?.onUnknownCost !== undefined
527
+ ? { onUnknownCost: raw.limits.onUnknownCost }
528
+ : {}),
529
+ },
530
+ unknownEvidence: normalizedUnknownEvidence(raw),
531
+ ...(compatibility ? { compatibility } : {}),
532
+ };
533
+ return { ...profile, revision: profileRevision(profile) };
534
+ }
535
+
536
+ export function getRoutingProfile(
537
+ config: { routingProfiles?: Record<string, OcxRoutingProfileConfig> },
538
+ id: string,
539
+ ): NormalizedRoutingProfile | undefined {
540
+ const profiles = config.routingProfiles;
541
+ if (!profiles || !Object.hasOwn(profiles, id)) return undefined;
542
+ return normalizeRoutingProfile(id, profiles[id]!);
543
+ }
544
+
545
+ export function listRoutingProfileIds(config: { routingProfiles?: Record<string, OcxRoutingProfileConfig> }): string[] {
546
+ return Object.keys(config.routingProfiles ?? {}).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
547
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Quota-aware policy scoring (RI-07).
3
+ *
4
+ * Evidence reuses the existing privacy-safe quota sources: the Codex pool
5
+ * account quota cache (usage percents only - no emails, tokens, or raw
6
+ * responses) and the provider per-account quota cache (Anthropic). Unknown
7
+ * quota stays unknown; no raw quota response ever reaches a trace.
8
+ *
9
+ * Boundary: policy profiles select provider/model TARGETS. Exact account
10
+ * selectors and Codex pool strategies remain authoritative inside their
11
+ * existing scope; account-level quota evidence is consumed when a candidate
12
+ * carries an account reference (dry-run/evaluate), never invented.
13
+ */
14
+
15
+ import {
16
+ codexQuotaWindowForPlan,
17
+ getAccountQuota,
18
+ isCodexQuotaExhausted,
19
+ listAccountQuotas,
20
+ } from "../codex/quota";
21
+ import { getCachedProviderAccountQuota } from "../providers/quota";
22
+ import type { RouteQuotaEvidence } from "./trace";
23
+
24
+ export interface QuotaEvidenceInput {
25
+ provider: string;
26
+ model: string;
27
+ accountRef?: string;
28
+ codexAccountId?: string;
29
+ codexAccountPlan?: string;
30
+ }
31
+
32
+ export interface CodexPoolQuotaAccount {
33
+ accountId: string;
34
+ plan?: string;
35
+ }
36
+
37
+ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuotaEvidence {
38
+ const quota = getAccountQuota(accountId);
39
+ if (!quota) return { known: false };
40
+ const monthly = codexQuotaWindowForPlan(plan) === "monthly";
41
+ const percents = [
42
+ ...(monthly ? [] : [quota.weeklyPercent]),
43
+ quota.monthlyPercent,
44
+ ].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
45
+ const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
46
+ // Credits-only snapshots prove neither usage nor exhaustion. Unknown must not
47
+ // become healthy capacity merely because a cache row exists.
48
+ if (maxPercent === undefined) return { known: false };
49
+ const resets = [
50
+ ...(monthly ? [] : [quota.weeklyResetAt]),
51
+ quota.monthlyResetAt,
52
+ ].filter((value): value is number => typeof value === "number" && Number.isFinite(value))
53
+ .filter(value => value > Date.now());
54
+ return {
55
+ known: true,
56
+ headroom: Math.max(0, Math.min(1, 1 - maxPercent / 100)),
57
+ exhausted: isCodexQuotaExhausted(quota, plan),
58
+ ...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
59
+ source: "codex-pool",
60
+ };
61
+ }
62
+
63
+ export function codexPoolQuotaEvidence(accounts: readonly CodexPoolQuotaAccount[]): RouteQuotaEvidence {
64
+ if (accounts.length === 0) return { known: false };
65
+ const evidence = accounts.map(account => codexAccountQuotaEvidence(account.accountId, account.plan));
66
+ const known = evidence.filter(item => item.known);
67
+ if (known.length === 0) return { known: false };
68
+ const usable = known.filter(item => item.exhausted !== true);
69
+ if (usable.length > 0) {
70
+ const headrooms = usable.map(item => item.headroom)
71
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
72
+ return {
73
+ known: true,
74
+ exhausted: false,
75
+ ...(headrooms.length > 0 ? { headroom: Math.max(...headrooms) } : {}),
76
+ source: "codex-pool",
77
+ };
78
+ }
79
+ if (known.length < evidence.length) return { known: false };
80
+ const resets = known.map(item => item.resetAtMs)
81
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
82
+ return {
83
+ known: true,
84
+ exhausted: true,
85
+ headroom: 0,
86
+ ...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
87
+ source: "codex-pool",
88
+ };
89
+ }
90
+
91
+ export function quotaEvidenceForCandidate(input: QuotaEvidenceInput): RouteQuotaEvidence {
92
+ if (input.provider === "openai" && input.codexAccountId) {
93
+ // listAccountQuotas() is the reconciled quota snapshot: config-generation
94
+ // reconciliation prunes removed accounts. Other eligibility dimensions
95
+ // (pause/reauth/cooldown/soft-avoid) remain health/pool-selector concerns.
96
+ if (input.codexAccountPlan !== undefined) {
97
+ const pool = [...listAccountQuotas()].map(([accountId]) => ({
98
+ accountId,
99
+ ...(accountId === input.codexAccountId ? { plan: input.codexAccountPlan } : {}),
100
+ }));
101
+ if (pool.length > 1) return codexPoolQuotaEvidence(pool);
102
+ }
103
+ return codexAccountQuotaEvidence(input.codexAccountId, input.codexAccountPlan);
104
+ }
105
+
106
+ if (input.provider === "anthropic" && input.accountRef) {
107
+ const quota = getCachedProviderAccountQuota("anthropic", input.accountRef);
108
+ if (quota) {
109
+ const family = anthropicFamilyWindow(input.model, quota.customWindows ?? []);
110
+ const percents = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, family?.percent]
111
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
112
+ const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
113
+ const resets = [quota.fiveHourResetAt, quota.weeklyResetAt, quota.monthlyResetAt, family?.resetAt]
114
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value))
115
+ .filter(value => value > Date.now());
116
+ return {
117
+ known: true,
118
+ ...(maxPercent !== undefined ? { headroom: Math.max(0, Math.min(1, 1 - maxPercent / 100)) } : {}),
119
+ exhausted: maxPercent !== undefined && maxPercent >= 100,
120
+ ...(resets.length > 0 ? { resetAtMs: Math.min(...resets) } : {}),
121
+ source: "provider-report",
122
+ };
123
+ }
124
+ }
125
+ return { known: false };
126
+ }
127
+
128
+ function anthropicFamilyWindow(
129
+ model: string,
130
+ windows: Array<{ label: string; percent?: number; resetAt?: number }>,
131
+ ): { percent?: number; resetAt?: number } | undefined {
132
+ const normalized = model.toLowerCase();
133
+ for (const window of windows) {
134
+ const family = window.label.trim().toLowerCase();
135
+ if (family && normalized.includes(family)) return window;
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ export function quotaScore(evidence: RouteQuotaEvidence | undefined): number | null {
141
+ if (!evidence || !evidence.known) return null;
142
+ if (evidence.exhausted === true) return 0;
143
+ if (typeof evidence.headroom === "number") return Math.max(0, Math.min(1, evidence.headroom));
144
+ return null;
145
+ }