@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -0,0 +1,444 @@
1
+ /**
2
+ * Deterministic policy-profile evaluator (RI-04 core; extended by RI-05..08).
3
+ *
4
+ * RI-04 evaluates hard capability requirements against supplied evidence and
5
+ * scores by deterministic configured priority only. It never dispatches an
6
+ * upstream request; execution wiring arrives with RI-05.
7
+ */
8
+
9
+ import type { OcxConfig } from "../types";
10
+ import {
11
+ buildRouteDecisionTrace,
12
+ MAX_REQUIREMENTS,
13
+ type RouteCapabilityEvidence,
14
+ type RouteCostEvidence,
15
+ type RouteDecisionTraceV1,
16
+ type RouteExclusionReason,
17
+ type RouteHealthEvidence,
18
+ type RouteQuotaEvidence,
19
+ type RouteRequirementEvidence,
20
+ type RouteScoreEvidence,
21
+ type Unknownable,
22
+ } from "./trace";
23
+ import { getRoutingProfile, policyModelId, type NormalizedRoutingProfile } from "./profile";
24
+ import { healthScore } from "./health";
25
+ import { quotaScore } from "./quota";
26
+ import { costScore } from "./cost";
27
+
28
+ /** Unknown health under "penalize": a low-but-not-zero deterministic floor. */
29
+ export const HEALTH_UNKNOWN_PENALTY_SCORE = 0.3;
30
+ /** Unknown health under "allow": neutral midpoint of the [0,1] health scale. */
31
+ export const HEALTH_UNKNOWN_NEUTRAL_SCORE = 0.5;
32
+ /** Unknown quota under "penalize": deterministic low floor. */
33
+ export const QUOTA_UNKNOWN_PENALTY_SCORE = 0.3;
34
+ /** Unknown cost under "penalize": deterministic low floor. */
35
+ export const COST_UNKNOWN_PENALTY_SCORE = 0.3;
36
+
37
+ export interface PolicyRequestEvidence {
38
+ /** Required context window for this request (tokens). */
39
+ contextWindow?: number;
40
+ toolsRequired?: boolean;
41
+ imageInputRequired?: boolean;
42
+ structuredOutputRequired?: boolean;
43
+ reasoningEffort?: string;
44
+ serviceTier?: string;
45
+ encryptedCodexTask?: boolean;
46
+ }
47
+
48
+ export interface PolicyCandidateEvidence {
49
+ provider: string;
50
+ model: string;
51
+ accountRef?: string;
52
+ /** Codex pool account id (provider "openai"); used to derive account-scoped quota evidence. */
53
+ codexAccountId?: string;
54
+ capability?: RouteCapabilityEvidence;
55
+ health?: RouteHealthEvidence;
56
+ quota?: RouteQuotaEvidence;
57
+ cost?: RouteCostEvidence;
58
+ }
59
+
60
+ export interface PolicyEvaluationCandidate {
61
+ provider: string;
62
+ model: string;
63
+ accountRef?: string;
64
+ eligible: boolean;
65
+ exclusions: RouteExclusionReason[];
66
+ requirements: RouteRequirementEvidence[];
67
+ capability?: RouteCapabilityEvidence;
68
+ health?: RouteHealthEvidence;
69
+ quota?: RouteQuotaEvidence;
70
+ cost?: RouteCostEvidence;
71
+ score?: RouteScoreEvidence;
72
+ }
73
+
74
+ export interface PolicyEvaluationResult {
75
+ profileId: string;
76
+ profileRevision: string;
77
+ candidates: PolicyEvaluationCandidate[];
78
+ selectedIndex: number | null;
79
+ trace: RouteDecisionTraceV1;
80
+ }
81
+
82
+ function booleanRequirement(
83
+ id: string,
84
+ required: boolean | undefined,
85
+ actual: Unknownable | undefined,
86
+ ): RouteRequirementEvidence | null {
87
+ if (required === undefined) return null;
88
+ // Note: `required === false` is a negative assertion ("must NOT have X"),
89
+ // not "don't care"; an absent field is the only "no requirement" form.
90
+ if (actual === undefined || actual === "unknown") {
91
+ return { id, expected: required, outcome: "unknown" };
92
+ }
93
+ return {
94
+ id,
95
+ expected: required,
96
+ actual: typeof actual === "boolean" ? actual : String(actual),
97
+ outcome: typeof actual === "boolean" && actual === required ? "satisfied" : "unsatisfied",
98
+ };
99
+ }
100
+
101
+ function requirementFor(
102
+ require: NormalizedRoutingProfile["require"],
103
+ capability: RouteCapabilityEvidence | undefined,
104
+ quota: RouteQuotaEvidence | undefined,
105
+ ): RouteRequirementEvidence[] {
106
+ const requirements: RouteRequirementEvidence[] = [];
107
+ if (require.minContextWindow !== undefined) {
108
+ const actual = capability?.contextWindow;
109
+ if (typeof actual === "number") {
110
+ requirements.push({
111
+ id: "min-context-window",
112
+ expected: require.minContextWindow,
113
+ actual,
114
+ outcome: actual >= require.minContextWindow ? "satisfied" : "unsatisfied",
115
+ });
116
+ } else {
117
+ requirements.push({ id: "min-context-window", expected: require.minContextWindow, outcome: "unknown" });
118
+ }
119
+ }
120
+ // minQuotaHeadroom gates only KNOWN quota via the normalized score, which
121
+ // maps exhaustion to zero and unknown/incomplete evidence to null. Unknown
122
+ // quota is governed by the profile's `unknownEvidence.quota` policy
123
+ // (exclude / penalize / allow) via the quota score path - never by the
124
+ // capability unknown policy.
125
+ if (require.minQuotaHeadroom !== undefined) {
126
+ const quotaHeadroom = quotaScore(quota);
127
+ if (quotaHeadroom !== null) {
128
+ requirements.push({
129
+ id: "min-quota-headroom",
130
+ expected: require.minQuotaHeadroom,
131
+ actual: quotaHeadroom,
132
+ outcome: quotaHeadroom >= require.minQuotaHeadroom ? "satisfied" : "unsatisfied",
133
+ });
134
+ }
135
+ }
136
+ const tools = booleanRequirement("tools", require.tools, capability?.tools);
137
+ if (tools) requirements.push(tools);
138
+ const image = booleanRequirement("image-input", require.imageInput, capability?.image);
139
+ if (image) requirements.push(image);
140
+ const structured = booleanRequirement("structured-output", require.structuredOutput, capability?.structuredOutput);
141
+ if (structured) requirements.push(structured);
142
+ if (require.reasoningEffort !== undefined) {
143
+ const ladder = capability?.reasoningEfforts;
144
+ if (Array.isArray(ladder)) {
145
+ requirements.push({
146
+ id: "reasoning-effort",
147
+ expected: require.reasoningEffort,
148
+ actual: ladder.join(","),
149
+ outcome: ladder.includes(require.reasoningEffort) ? "satisfied" : "unsatisfied",
150
+ });
151
+ } else {
152
+ requirements.push({ id: "reasoning-effort", expected: require.reasoningEffort, outcome: "unknown" });
153
+ }
154
+ }
155
+ if (require.serviceTier !== undefined) {
156
+ const actual = capability?.serviceTier;
157
+ if (actual === undefined || actual === "unknown") {
158
+ requirements.push({ id: "service-tier", expected: require.serviceTier, outcome: "unknown" });
159
+ } else {
160
+ requirements.push({
161
+ id: "service-tier",
162
+ expected: require.serviceTier,
163
+ actual: String(actual),
164
+ outcome: actual === require.serviceTier ? "satisfied" : "unsatisfied",
165
+ });
166
+ }
167
+ }
168
+ const local = booleanRequirement("local-only", require.localOnly, capability?.localOnly);
169
+ if (local) requirements.push(local);
170
+ const remote = booleanRequirement("remote-allowed", require.remoteAllowed, capability?.remoteAllowed);
171
+ if (remote) requirements.push(remote);
172
+ const encrypted = booleanRequirement(
173
+ "encrypted-codex-tasks",
174
+ require.encryptedCodexTasks,
175
+ capability?.encryptedCodexTasks,
176
+ );
177
+ if (encrypted) requirements.push(encrypted);
178
+ return requirements;
179
+ }
180
+
181
+ function unsatisfiedOrUnknown(requirements: RouteRequirementEvidence[]): RouteRequirementEvidence[] {
182
+ return requirements.filter(requirement => requirement.outcome !== "satisfied");
183
+ }
184
+
185
+ function configuredPriorityScore(index: number, total: number): number {
186
+ return total > 1 ? (total - index) / total : 1;
187
+ }
188
+
189
+ /**
190
+ * Requirements derived from the request evidence supplied to a dry-run. A
191
+ * candidate must satisfy both the profile `require` block and the request
192
+ * needs (context window, tools, image input, structured output, reasoning
193
+ * effort, service tier, encrypted Codex tasks) to be eligible.
194
+ */
195
+ function requestRequirementFor(
196
+ request: PolicyRequestEvidence,
197
+ capability: RouteCapabilityEvidence | undefined,
198
+ ): RouteRequirementEvidence[] {
199
+ const requirements: RouteRequirementEvidence[] = [];
200
+ if (request.contextWindow !== undefined) {
201
+ const actual = capability?.contextWindow;
202
+ if (typeof actual === "number") {
203
+ requirements.push({
204
+ id: "request-context-window",
205
+ expected: request.contextWindow,
206
+ actual,
207
+ outcome: actual >= request.contextWindow ? "satisfied" : "unsatisfied",
208
+ });
209
+ } else {
210
+ requirements.push({ id: "request-context-window", expected: request.contextWindow, outcome: "unknown" });
211
+ }
212
+ }
213
+ // Absent flags mean "no requirement": only a positive request need adds a row.
214
+ if (request.toolsRequired === true) {
215
+ const tools = booleanRequirement("request-tools", true, capability?.tools);
216
+ if (tools) requirements.push(tools);
217
+ }
218
+ if (request.imageInputRequired === true) {
219
+ const image = booleanRequirement("request-image-input", true, capability?.image);
220
+ if (image) requirements.push(image);
221
+ }
222
+ if (request.structuredOutputRequired === true) {
223
+ const structured = booleanRequirement("request-structured-output", true, capability?.structuredOutput);
224
+ if (structured) requirements.push(structured);
225
+ }
226
+ if (request.reasoningEffort !== undefined) {
227
+ const ladder = capability?.reasoningEfforts;
228
+ if (Array.isArray(ladder)) {
229
+ requirements.push({
230
+ id: "request-reasoning-effort",
231
+ expected: request.reasoningEffort,
232
+ actual: ladder.join(","),
233
+ outcome: ladder.includes(request.reasoningEffort) ? "satisfied" : "unsatisfied",
234
+ });
235
+ } else {
236
+ requirements.push({ id: "request-reasoning-effort", expected: request.reasoningEffort, outcome: "unknown" });
237
+ }
238
+ }
239
+ if (request.serviceTier !== undefined) {
240
+ const actual = capability?.serviceTier;
241
+ if (actual === undefined || actual === "unknown") {
242
+ requirements.push({ id: "request-service-tier", expected: request.serviceTier, outcome: "unknown" });
243
+ } else {
244
+ requirements.push({
245
+ id: "request-service-tier",
246
+ expected: request.serviceTier,
247
+ actual: String(actual),
248
+ outcome: actual === request.serviceTier ? "satisfied" : "unsatisfied",
249
+ });
250
+ }
251
+ }
252
+ if (request.encryptedCodexTask === true) {
253
+ const encrypted = booleanRequirement(
254
+ "request-encrypted-codex-tasks",
255
+ true,
256
+ capability?.encryptedCodexTasks,
257
+ );
258
+ if (encrypted) requirements.push(encrypted);
259
+ }
260
+ return requirements;
261
+ }
262
+
263
+ /**
264
+ * Evaluate a profile against request + candidate evidence. Deterministic:
265
+ * candidates are scored in declaration order, ties break by earlier index.
266
+ */
267
+ export function evaluatePolicyProfile(
268
+ config: OcxConfig,
269
+ profileId: string,
270
+ requestEvidence: PolicyRequestEvidence,
271
+ candidateEvidence: PolicyCandidateEvidence[],
272
+ now = Date.now(),
273
+ ): PolicyEvaluationResult {
274
+ const profile = getRoutingProfile(config, profileId);
275
+ if (!profile) throw new Error(`Unknown routing profile: ${profileId}`);
276
+
277
+ const candidates: PolicyEvaluationCandidate[] = [];
278
+ let selectedIndex: number | null = null;
279
+ let bestScore = Number.NEGATIVE_INFINITY;
280
+
281
+ profile.candidates.forEach((declared, index) => {
282
+ const evidence = candidateEvidence.find(
283
+ candidate => candidate.provider === declared.provider && candidate.model === declared.model,
284
+ ) ?? { provider: declared.provider, model: declared.model };
285
+ const requirements = [
286
+ ...requirementFor(profile.require, evidence.capability, evidence.quota),
287
+ ...requestRequirementFor(requestEvidence, evidence.capability),
288
+ ];
289
+ const exclusions: RouteExclusionReason[] = [];
290
+ const bad = unsatisfiedOrUnknown(requirements);
291
+ for (const requirement of bad) {
292
+ if (requirement.outcome === "unsatisfied") {
293
+ exclusions.push({ code: "capability-unsatisfied", detail: requirement.id });
294
+ } else {
295
+ exclusions.push({ code: "unknown-capability", detail: requirement.id });
296
+ }
297
+ }
298
+ const unsatisfied = bad.some(requirement => requirement.outcome === "unsatisfied");
299
+ const unknown = bad.some(requirement => requirement.outcome === "unknown");
300
+ // Unknown capability handling per profile: exclude (default), penalize,
301
+ // or allow. "penalize" cannot move the score while configuredPriorityScore
302
+ // is the only component; the capability score dimension is still future
303
+ // (RI-06+), so "penalize" behaves identically to "allow" in this release.
304
+ const excludedByUnknown = unknown && profile.unknownEvidence.capability === "exclude";
305
+ const costLimit = profile.limits.maxEstimatedCostUsd;
306
+ const estimatedCost = evidence.cost?.estimatedUsd;
307
+ const overCostLimit = costLimit !== undefined
308
+ && typeof estimatedCost === "number"
309
+ && Number.isFinite(estimatedCost)
310
+ && estimatedCost > costLimit;
311
+ if (overCostLimit) {
312
+ exclusions.push({ code: "cost-limit", detail: "maxEstimatedCostUsd" });
313
+ }
314
+ let eligible = !unsatisfied && !excludedByUnknown && !overCostLimit;
315
+
316
+ // Health scoring (RI-06): live hard cooldown is authoritative and
317
+ // excludes; unknown health follows the profile's unknownEvidence policy;
318
+ // historical health never overrides explicit ineligibility.
319
+ const health = evidence.health;
320
+ let healthValue = health ? healthScore(health, now) : null;
321
+ if (health?.cooldownUntilMs !== undefined && health.cooldownUntilMs > now) {
322
+ exclusions.push({ code: "cooldown" });
323
+ eligible = false;
324
+ } else if (healthValue === null && profile.unknownEvidence.health === "exclude") {
325
+ exclusions.push({ code: "unknown-health" });
326
+ eligible = false;
327
+ } else if (healthValue === null && profile.unknownEvidence.health === "penalize") {
328
+ healthValue = HEALTH_UNKNOWN_PENALTY_SCORE;
329
+ } else if (healthValue === null && profile.unknownEvidence.health === "allow") {
330
+ // Neutral midpoint: blending keeps an unknown candidate from outranking
331
+ // a measured one with the same configured priority.
332
+ healthValue = HEALTH_UNKNOWN_NEUTRAL_SCORE;
333
+ }
334
+
335
+ // Quota scoring (RI-07): unknown quota follows the profile policy;
336
+ // exhausted or low-headroom evidence lowers the score.
337
+ const quota = evidence.quota;
338
+ let quotaValue = quota ? quotaScore(quota) : null;
339
+ if (quotaValue === null) {
340
+ if (profile.unknownEvidence.quota === "exclude") {
341
+ exclusions.push({ code: "unknown-quota" });
342
+ eligible = false;
343
+ } else if (profile.unknownEvidence.quota === "penalize") {
344
+ quotaValue = QUOTA_UNKNOWN_PENALTY_SCORE;
345
+ }
346
+ }
347
+
348
+ // Cost scoring (RI-08): the hard per-request ceiling was already checked
349
+ // above; unknown cost follows the profile's unknownEvidence policy.
350
+ const cost = evidence.cost;
351
+ let costValue = cost ? costScore(cost) : null;
352
+ if (costValue === null && profile.unknownEvidence.cost === "exclude") {
353
+ exclusions.push({ code: "unknown-price" });
354
+ eligible = false;
355
+ } else if (costValue === null && profile.unknownEvidence.cost === "penalize") {
356
+ costValue = COST_UNKNOWN_PENALTY_SCORE;
357
+ }
358
+
359
+ const priorityScore = configuredPriorityScore(index, profile.candidates.length);
360
+ const healthWeight = profile.optimize.health;
361
+ const quotaWeight = profile.optimize.quota;
362
+ const costWeight = profile.optimize.cost;
363
+ // Only spend a dimension's weight when a value is actually present:
364
+ // "allow" leaves missing health/quota/cost components null, so subtracting
365
+ // their weights would shrink the priority share for evidence the profile
366
+ // explicitly permits to be absent. Renormalize those weights back into
367
+ // priority instead of silently changing the ranking semantics.
368
+ const spentHealth = healthValue !== null ? healthWeight : 0;
369
+ const spentQuota = quotaValue !== null ? quotaWeight : 0;
370
+ const spentCost = costValue !== null ? costWeight : 0;
371
+ const priorityWeight = Math.max(0, 1 - spentHealth - spentQuota - spentCost);
372
+ const components: RouteScoreEvidence["components"] = { configuredPriority: priorityScore };
373
+ let total = priorityWeight * priorityScore;
374
+ if (healthWeight > 0 && healthValue !== null) {
375
+ total += healthWeight * healthValue;
376
+ components.health = healthValue;
377
+ }
378
+ if (quotaWeight > 0 && quotaValue !== null) {
379
+ total += quotaWeight * quotaValue;
380
+ components.quota = quotaValue;
381
+ }
382
+ if (costWeight > 0 && costValue !== null) {
383
+ total += costWeight * costValue;
384
+ components.cost = costValue;
385
+ }
386
+ const score: RouteScoreEvidence = { total, components };
387
+ const evaluated: PolicyEvaluationCandidate = {
388
+ provider: evidence.provider,
389
+ model: evidence.model,
390
+ ...(evidence.accountRef ? { accountRef: evidence.accountRef } : {}),
391
+ eligible,
392
+ exclusions,
393
+ requirements,
394
+ ...(evidence.capability ? { capability: evidence.capability } : {}),
395
+ ...(evidence.health ? { health: evidence.health } : {}),
396
+ ...(evidence.quota ? { quota: evidence.quota } : {}),
397
+ ...(evidence.cost ? { cost: evidence.cost } : {}),
398
+ score,
399
+ };
400
+ candidates.push(evaluated);
401
+
402
+ if (evaluated.eligible && score.total > bestScore) {
403
+ bestScore = score.total;
404
+ selectedIndex = index;
405
+ }
406
+ });
407
+
408
+ const trace = buildRouteDecisionTrace({
409
+ requestedModel: policyModelId(profileId),
410
+ routeKind: "policy",
411
+ profile: { id: profile.id, revision: profile.revision },
412
+ // Flat, capped summary (truncation is flagged by the builder); per-candidate
413
+ // attribution lives in each candidate's `requirements`/`exclusions`.
414
+ requirements: candidates.flatMap(candidate => candidate.requirements).slice(0, MAX_REQUIREMENTS),
415
+ candidates: candidates.map(candidate => ({
416
+ provider: candidate.provider,
417
+ model: candidate.model,
418
+ ...(candidate.accountRef ? { accountRef: candidate.accountRef } : {}),
419
+ eligible: candidate.eligible,
420
+ exclusions: candidate.exclusions,
421
+ ...(candidate.score ? { score: candidate.score } : {}),
422
+ ...(candidate.capability ? { capability: candidate.capability } : {}),
423
+ ...(candidate.health ? { health: candidate.health } : {}),
424
+ ...(candidate.quota ? { quota: candidate.quota } : {}),
425
+ ...(candidate.cost ? { cost: candidate.cost } : {}),
426
+ })),
427
+ selected: selectedIndex === null
428
+ ? { provider: candidates[0]?.provider ?? "", model: candidates[0]?.model ?? "", reason: "no-eligible-candidate" }
429
+ : {
430
+ candidateIndex: selectedIndex,
431
+ provider: candidates[selectedIndex]!.provider,
432
+ model: candidates[selectedIndex]!.model,
433
+ reason: "policy-selected",
434
+ },
435
+ });
436
+
437
+ return {
438
+ profileId: profile.id,
439
+ profileRevision: profile.revision,
440
+ candidates,
441
+ selectedIndex,
442
+ trace,
443
+ };
444
+ }