@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -9,19 +9,42 @@
9
9
  * assistant message is about to serialize without thinking parts (compacted
10
10
  * history, lost assistant turn, orphan-repaired tool results).
11
11
  *
12
- * Entries are scoped by an optional conversation identity in addition to the
13
- * call id: provider-generated ids like `call_1` are not globally unique, so a
14
- * process-wide key would let one conversation's reasoning bleed into another
15
- * when ids collide (CodeRabbit P1 on #971).
12
+ * Entries require a conversation identity in addition to the call id:
13
+ * provider-generated ids like `call_1` are not globally unique, so an
14
+ * unscoped process-wide key would let one conversation's reasoning bleed into
15
+ * another when ids collide (CodeRabbit P1 on #971).
16
16
  *
17
17
  * Privacy: entries hold reasoning text in memory only — never logged,
18
18
  * serialized, or exported. Bounded by entry count, total bytes, and TTL, so a
19
19
  * long-lived proxy cannot grow without limit.
20
20
  */
21
21
 
22
+ import { createHmac, randomBytes } from "node:crypto";
23
+ import type {
24
+ OcxProviderConfig,
25
+ OcxReasoningReplayIdentity,
26
+ OcxReasoningReplayScopeRef,
27
+ } from "../types";
28
+
22
29
  const MAX_ENTRIES = 64;
23
30
  const MAX_TOTAL_BYTES = 256 * 1024;
24
31
  const TTL_MS = 60 * 60 * 1000;
32
+ const replayIdentityKey = randomBytes(32);
33
+ const CREDENTIAL_HEADER_NAMES = new Set([
34
+ "authorization",
35
+ "api-key",
36
+ "chatgpt-account-id",
37
+ "cookie",
38
+ "openai-organization",
39
+ "openai-project",
40
+ "proxy-authorization",
41
+ "x-api-key",
42
+ "x-api-token",
43
+ "x-auth-token",
44
+ "x-goog-api-key",
45
+ "x-openai-organization",
46
+ "x-openai-project",
47
+ ]);
25
48
 
26
49
  interface CacheEntry {
27
50
  text: string;
@@ -34,8 +57,131 @@ let totalBytes = 0;
34
57
  let clockForTests: (() => number) | null = null;
35
58
 
36
59
  const now = (): number => clockForTests?.() ?? Date.now();
37
- const keyFor = (callId: string, scope: string | undefined): string =>
38
- `${scope ?? "global"}\u0000${callId}`;
60
+
61
+ function nonEmpty(value: unknown): value is string {
62
+ return typeof value === "string" && value.trim().length > 0;
63
+ }
64
+
65
+ function keyFor(callId: string, scope: OcxReasoningReplayScopeRef | undefined): string | undefined {
66
+ const identity = scope?.current;
67
+ if (
68
+ !nonEmpty(callId)
69
+ || !nonEmpty(scope?.clientThreadId)
70
+ || !nonEmpty(identity?.providerName)
71
+ || !nonEmpty(identity?.providerDestinationIdentity)
72
+ || !nonEmpty(identity?.adapterName)
73
+ || !nonEmpty(identity?.modelId)
74
+ || !nonEmpty(identity?.credentialIdentity)
75
+ ) return undefined;
76
+ return JSON.stringify([
77
+ scope.clientThreadId,
78
+ identity.providerName,
79
+ identity.providerDestinationIdentity,
80
+ identity.adapterName,
81
+ identity.modelId,
82
+ identity.credentialIdentity,
83
+ callId,
84
+ ]);
85
+ }
86
+
87
+ function processLocalIdentity(domain: string, material: string): string {
88
+ return createHmac("sha256", replayIdentityKey)
89
+ .update(domain)
90
+ .update("\0")
91
+ .update(material)
92
+ .digest("hex");
93
+ }
94
+
95
+ function credentialHeaderOverrides(headers: Record<string, string> | undefined): [string, string][] {
96
+ return Object.entries(headers ?? {})
97
+ .filter(([name, value]) => CREDENTIAL_HEADER_NAMES.has(name.trim().toLowerCase()) && nonEmpty(value))
98
+ .map(([name, value]) => [name.trim().toLowerCase(), value] as [string, string])
99
+ .sort(([leftName, leftValue], [rightName, rightValue]) => (
100
+ leftName.localeCompare(rightName) || leftValue.localeCompare(rightValue)
101
+ ));
102
+ }
103
+
104
+ /** Produce a non-reversible process-local identity for an exact upstream destination. */
105
+ export function reasoningReplayDestinationIdentity(baseUrl: string | undefined): string | undefined {
106
+ if (!nonEmpty(baseUrl)) return undefined;
107
+ const canonical = baseUrl.trim().replace(/\/+$/, "");
108
+ return `destination:${processLocalIdentity("destination", canonical)}`;
109
+ }
110
+
111
+ /** Produce a non-reversible process-local identity for credential material. */
112
+ export function reasoningReplayCredentialIdentity(
113
+ kind: "key" | "oauth" | "codex",
114
+ material: string | undefined,
115
+ headers?: Record<string, string>,
116
+ ): string | undefined {
117
+ if (!nonEmpty(material)) return undefined;
118
+ const overrides = credentialHeaderOverrides(headers);
119
+ return `${kind}:${processLocalIdentity(`credential:${kind}`, JSON.stringify([material, overrides]))}`;
120
+ }
121
+
122
+ /** Bind Codex forwarding to the effective bearer/account and selected physical pool slot. */
123
+ export function reasoningReplayCodexCredentialIdentity(args: {
124
+ authorization?: string | null;
125
+ chatgptAccountId?: string | null;
126
+ accountId?: string | null;
127
+ credentialGeneration?: number | string | null;
128
+ writerGeneration?: number | string | null;
129
+ headers?: Record<string, string>;
130
+ }): string | undefined {
131
+ const configuredAuthorization = credentialHeaderOverrides(args.headers)
132
+ .find(([name]) => name === "authorization")?.[1];
133
+ const authorization = nonEmpty(args.authorization) ? args.authorization : configuredAuthorization;
134
+ if (!authorization) return undefined;
135
+ const material = JSON.stringify([
136
+ authorization,
137
+ nonEmpty(args.chatgptAccountId) ? args.chatgptAccountId : "",
138
+ nonEmpty(args.accountId) ? args.accountId : "",
139
+ args.credentialGeneration === null || args.credentialGeneration === undefined
140
+ ? ""
141
+ : String(args.credentialGeneration),
142
+ args.writerGeneration === null || args.writerGeneration === undefined
143
+ ? ""
144
+ : String(args.writerGeneration),
145
+ ]);
146
+ return reasoningReplayCredentialIdentity("codex", material, args.headers);
147
+ }
148
+
149
+ /** Bind OAuth replay to one persisted credential slot and exact token generation. */
150
+ export function reasoningReplayOAuthCredentialIdentity(
151
+ snapshot: Readonly<{ accountId: string; generation: string }> | undefined,
152
+ headers?: Record<string, string>,
153
+ ): string | undefined {
154
+ if (!snapshot || !nonEmpty(snapshot.accountId) || !nonEmpty(snapshot.generation)) return undefined;
155
+ return reasoningReplayCredentialIdentity(
156
+ "oauth",
157
+ `${snapshot.accountId}\0${snapshot.generation}`,
158
+ headers,
159
+ );
160
+ }
161
+
162
+ /** Bind key-auth provider material without putting raw secrets in the replay key. */
163
+ export function reasoningReplayKeyCredentialIdentity(
164
+ provider: Pick<OcxProviderConfig, "apiKey" | "headers">,
165
+ ): string | undefined {
166
+ const apiKey = nonEmpty(provider.apiKey) ? provider.apiKey : undefined;
167
+ // Public/static headers do not establish a physical credential boundary.
168
+ // Header-only/key-optional providers therefore fail closed for replay.
169
+ if (!apiKey) return undefined;
170
+ return reasoningReplayCredentialIdentity("key", apiKey, provider.headers);
171
+ }
172
+
173
+ /** Replace only the holder snapshot so parsed-request copies observe rotations. */
174
+ export function bindReasoningReplayScope(
175
+ scope: OcxReasoningReplayScopeRef | undefined,
176
+ identity: OcxReasoningReplayIdentity | undefined,
177
+ ): void {
178
+ if (!scope) return;
179
+ if (!identity || !keyFor("binding-check", { ...scope, current: identity })) {
180
+ delete scope.current;
181
+ return;
182
+ }
183
+ scope.current = { ...identity };
184
+ }
39
185
 
40
186
  /**
41
187
  * Record the raw reasoning text that preceded the given tool call.
@@ -43,9 +189,18 @@ const keyFor = (callId: string, scope: string | undefined): string =>
43
189
  * Expired entries are swept on insert so the TTL bound holds even when a call
44
190
  * id is never read again.
45
191
  */
46
- export function rememberReasoningForCall(callId: string, text: string, scope?: string): void {
192
+ export function rememberReasoningForCall(
193
+ callId: string,
194
+ text: string,
195
+ scope?: OcxReasoningReplayScopeRef,
196
+ ): void {
197
+ // Never fall back to a process-wide namespace. Call ids are supplied by
198
+ // clients/providers and are therefore neither unique nor trustworthy; an
199
+ // unscoped entry could be recovered by an unrelated request that reuses the
200
+ // same id.
47
201
  // Empty provider deltas are absence of new reasoning, not a request to erase a candidate.
48
- if (!callId || typeof text !== "string" || text.length === 0) return;
202
+ const key = keyFor(callId, scope);
203
+ if (!key || typeof text !== "string" || text.length === 0) return;
49
204
  const bytes = Buffer.byteLength(text, "utf8");
50
205
  // A single entry larger than the whole budget would immediately evict itself.
51
206
  if (bytes > MAX_TOTAL_BYTES) return;
@@ -58,7 +213,6 @@ export function rememberReasoningForCall(callId: string, text: string, scope?: s
58
213
  totalBytes -= entry.bytes;
59
214
  }
60
215
  }
61
- const key = keyFor(callId, scope);
62
216
  const previous = entries.get(key);
63
217
  if (previous) totalBytes -= previous.bytes;
64
218
  entries.set(key, { text, bytes, at });
@@ -85,9 +239,12 @@ export function rememberReasoningForCall(callId: string, text: string, scope?: s
85
239
  * Read the recorded reasoning for a call id without removing it: retries after
86
240
  * a failed continuation reuse the same fallback.
87
241
  */
88
- export function peekReasoningForCall(callId: string, scope?: string): string | undefined {
89
- if (!callId) return undefined;
242
+ export function peekReasoningForCall(
243
+ callId: string,
244
+ scope?: OcxReasoningReplayScopeRef,
245
+ ): string | undefined {
90
246
  const key = keyFor(callId, scope);
247
+ if (!key) return undefined;
91
248
  const entry = entries.get(key);
92
249
  if (!entry) return undefined;
93
250
  if (now() - entry.at >= TTL_MS) {
@@ -107,7 +107,7 @@ export const toolSchema = z.object({
107
107
  const builtinToolSchema = z.object({ type: z.string() }).loose();
108
108
 
109
109
  const hostedToolType = z.enum([
110
- "web_search_preview", "file_search", "computer_use_preview",
110
+ "web_search", "web_search_preview", "file_search", "computer_use_preview",
111
111
  "code_interpreter", "image_generation", "mcp",
112
112
  ]);
113
113
 
package/src/router.ts CHANGED
@@ -31,10 +31,7 @@ import {
31
31
  } from "./routing/trace";
32
32
  import { getRoutingProfile, resolvePolicyProfileId } from "./routing/profile";
33
33
  import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
34
- import { candidateCapabilityEvidence } from "./routing/capability";
35
- import { policyCandidateHealthEvidence } from "./routing/health";
36
- import { quotaEvidenceForCandidate } from "./routing/quota";
37
- import { costEvidenceForCandidate } from "./routing/cost";
34
+ import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
38
35
 
39
36
  export class NoEligiblePolicyCandidateError extends Error {
40
37
  /** Evaluation trace (with per-candidate exclusions) when nothing qualified. */
@@ -250,7 +247,7 @@ function usableResolvedApiKey(apiKey: string | undefined): string | undefined {
250
247
  return typeof resolved === "string" && resolved.trim().length > 0 ? resolved : undefined;
251
248
  }
252
249
 
253
- function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
250
+ export function routedProviderConfig(providerName: string, provider: OcxProviderConfig): OcxProviderConfig {
254
251
  const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
255
252
  if (!registryEntry || !providerMatchesRegistryTransport(providerName, provider)) {
256
253
  assertProviderDestinationAllowed(providerName, provider);
@@ -512,21 +509,9 @@ function routeModelInternal(
512
509
  // One clock read per decision keeps candidate evidence, exclusions, and
513
510
  // scores mutually consistent and reproducible.
514
511
  const now = Date.now();
515
- const candidateEvidence = profile.candidates.map(candidate => ({
516
- provider: candidate.provider,
517
- model: candidate.model,
518
- capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model),
519
- health: policyCandidateHealthEvidence(config, candidate, now),
520
- quota: quotaEvidenceForCandidate({
521
- provider: candidate.provider,
522
- model: candidate.model,
523
- }),
524
- cost: costEvidenceForCandidate({
525
- provider: candidate.provider,
526
- model: candidate.model,
527
- limitUsd: profile.limits.maxEstimatedCostUsd,
528
- }),
529
- }));
512
+ const candidateEvidence = assemblePolicyCandidateEvidence(config, profile, now, {
513
+ routedProviderConfig,
514
+ });
530
515
  const evaluation = evaluatePolicyProfile(config, policyId, policyEvidence ?? {}, candidateEvidence, now);
531
516
  if (evaluation.selectedIndex === null) {
532
517
  throw new NoEligiblePolicyCandidateError(policyId, evaluation.trace);
@@ -11,7 +11,8 @@
11
11
  */
12
12
 
13
13
  import type { OcxConfig } from "../types";
14
- import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
14
+ import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
15
+ import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
15
16
  import { PROVIDER_REGISTRY } from "../providers/registry";
16
17
  import {
17
18
  nativeInputModalities,
@@ -145,13 +146,18 @@ export function candidateCapabilityEvidence(
145
146
  const provider = config.providers[providerName];
146
147
  const registryEntry = PROVIDER_REGISTRY.find(entry => entry.id === providerName);
147
148
  const catalogRow = cachedCatalogModels().find(model => model.provider === providerName && model.id === modelId);
148
- const isNative = providerName === "openai" && !modelId.includes("/");
149
+ const isNative = providerName === OPENAI_CODEX_PROVIDER_ID && !modelId.includes("/");
149
150
 
150
- const contextWindow = provider?.modelContextWindows?.[modelId]
151
+ const rawContextWindow = provider?.modelContextWindows?.[modelId]
151
152
  ?? provider?.contextWindow
152
153
  ?? registryEntry?.modelContextWindows?.[modelId]
153
154
  ?? catalogRow?.contextWindow
154
155
  ?? (isNative ? nativeOpenAiContextWindow(modelId) : undefined);
156
+ // providerContextCaps.openai also ceilings native OpenAI rows (#1430), so routing
157
+ // evidence never contradicts a capped catalog entry.
158
+ const contextWindow = isNative
159
+ ? applyProviderContextCap(rawContextWindow, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) ?? rawContextWindow
160
+ : rawContextWindow;
155
161
 
156
162
  const modalities = provider?.modelInputModalities?.[modelId]
157
163
  ?? registryEntry?.modelInputModalities?.[modelId]
@@ -0,0 +1,159 @@
1
+ import type { OcxConfig, OcxProviderConfig } from "../../types";
2
+ import { candidateCapabilityEvidence } from "../capability";
3
+ import { costEvidenceForCandidate } from "../cost";
4
+ import type { PolicyCandidateEvidence } from "../evaluator";
5
+ import { policyCandidateHealthEvidence } from "../health";
6
+ import type { NormalizedRoutingProfile } from "../profile";
7
+ import { quotaEvidenceForCandidate } from "../quota";
8
+ import {
9
+ compatibilitySuiteKey,
10
+ loadCompatibilityCatalogSnapshot,
11
+ type CompatibilityCatalogSnapshot,
12
+ } from "./catalog";
13
+ import { findVerdictForSuite, loadCompatibilityEvidenceSnapshot } from "./reader";
14
+ import {
15
+ resolvePolicyCompatibilitySubjects,
16
+ type ResolvedPolicyCompatibilitySubjects,
17
+ } from "./subject";
18
+ import type { CandidateCompatibilityEvidence } from "./types";
19
+
20
+ export type RoutedProviderResolver = (
21
+ providerName: string,
22
+ provider: OcxProviderConfig,
23
+ ) => OcxProviderConfig;
24
+
25
+ export interface AssemblePolicyEvidenceOptions {
26
+ configDir?: string;
27
+ routedProviderConfig: RoutedProviderResolver;
28
+ resolveSubjects?: typeof resolvePolicyCompatibilitySubjects;
29
+ loadEvidenceSnapshot?: typeof loadCompatibilityEvidenceSnapshot;
30
+ loadCatalogSnapshot?: typeof loadCompatibilityCatalogSnapshot;
31
+ }
32
+
33
+ function attachCompatibilityEvidence(
34
+ resolved: ResolvedPolicyCompatibilitySubjects | undefined,
35
+ snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot>,
36
+ catalog: CompatibilityCatalogSnapshot,
37
+ profile: NonNullable<NormalizedRoutingProfile["compatibility"]>,
38
+ ): CandidateCompatibilityEvidence {
39
+ const subjectIds = resolved?.subjectIds ?? {};
40
+ const suites: CandidateCompatibilityEvidence["suites"] = [];
41
+
42
+ for (const requirement of profile.requiredSuites) {
43
+ const subjectId = subjectIds[requirement.evidenceLayer];
44
+ if (!subjectId) continue;
45
+ const metadata = catalog.get(compatibilitySuiteKey(requirement.evidenceLayer, requirement.suiteId));
46
+ if (!metadata) continue;
47
+ const row = findVerdictForSuite(
48
+ snapshot,
49
+ subjectId,
50
+ requirement.evidenceLayer,
51
+ requirement.suiteId,
52
+ metadata.suiteVersion,
53
+ metadata.suiteManifestDigest,
54
+ );
55
+ if (!row) continue;
56
+ suites.push({
57
+ subjectId,
58
+ suiteId: row.suiteId,
59
+ evidenceLayer: requirement.evidenceLayer,
60
+ suiteVersion: row.suiteVersion,
61
+ suiteManifestDigest: row.suiteManifestDigest,
62
+ verdict: row.verdict,
63
+ asOf: row.asOf,
64
+ maxAgeMs: metadata.maxAgeMs,
65
+ notes: row.notes,
66
+ });
67
+ }
68
+
69
+ return {
70
+ subjectIds: { ...subjectIds },
71
+ projectionAvailable: snapshot.projectionAvailable,
72
+ suites,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Assemble production policy candidate evidence including compatibility snapshots.
78
+ * Compatibility work is completely skipped for legacy/no-requirement profiles.
79
+ * Active compatibility profiles use one catalog snapshot and one bounded SQLite
80
+ * read for all candidate subject IDs.
81
+ */
82
+ export function assemblePolicyCandidateEvidence(
83
+ config: OcxConfig,
84
+ profile: NormalizedRoutingProfile,
85
+ now: number,
86
+ options: AssemblePolicyEvidenceOptions,
87
+ ): PolicyCandidateEvidence[] {
88
+ const compatibilityPolicy = profile.compatibility;
89
+ const hasCompatibilityRequirements = Boolean(
90
+ compatibilityPolicy && compatibilityPolicy.requiredSuites.length > 0,
91
+ );
92
+ const resolvedByCandidate = new Map<string, ResolvedPolicyCompatibilitySubjects>();
93
+ let catalog: CompatibilityCatalogSnapshot = new Map();
94
+ let snapshot: ReturnType<typeof loadCompatibilityEvidenceSnapshot> = {
95
+ projectionAvailable: true,
96
+ projectionIncompatible: false,
97
+ bySubject: new Map(),
98
+ };
99
+
100
+ if (hasCompatibilityRequirements && compatibilityPolicy) {
101
+ const resolveSubjects = options.resolveSubjects ?? resolvePolicyCompatibilitySubjects;
102
+ const loadCatalog = options.loadCatalogSnapshot ?? loadCompatibilityCatalogSnapshot;
103
+ const loadEvidence = options.loadEvidenceSnapshot ?? loadCompatibilityEvidenceSnapshot;
104
+ catalog = loadCatalog(compatibilityPolicy.requiredSuites);
105
+ const subjectIds = new Set<string>();
106
+
107
+ for (const candidate of profile.candidates) {
108
+ const provider = config.providers[candidate.provider];
109
+ if (!provider) continue;
110
+ try {
111
+ const routed = options.routedProviderConfig(candidate.provider, provider);
112
+ const resolved = resolveSubjects(
113
+ config,
114
+ candidate.provider,
115
+ candidate.model,
116
+ routed,
117
+ options.configDir,
118
+ );
119
+ resolvedByCandidate.set(`${candidate.provider}/${candidate.model}`, resolved);
120
+ for (const subjectId of Object.values(resolved.subjectIds)) {
121
+ if (subjectId) subjectIds.add(subjectId);
122
+ }
123
+ } catch {
124
+ // Subject construction failure is handled per required layer as unknown.
125
+ }
126
+ }
127
+
128
+ snapshot = loadEvidence([...subjectIds], options.configDir);
129
+ }
130
+
131
+ return profile.candidates.map(candidate => {
132
+ const key = `${candidate.provider}/${candidate.model}`;
133
+ const compatibility = hasCompatibilityRequirements && compatibilityPolicy
134
+ ? attachCompatibilityEvidence(
135
+ resolvedByCandidate.get(key),
136
+ snapshot,
137
+ catalog,
138
+ compatibilityPolicy,
139
+ )
140
+ : undefined;
141
+
142
+ return {
143
+ provider: candidate.provider,
144
+ model: candidate.model,
145
+ capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model),
146
+ health: policyCandidateHealthEvidence(config, candidate, now),
147
+ quota: quotaEvidenceForCandidate({
148
+ provider: candidate.provider,
149
+ model: candidate.model,
150
+ }),
151
+ cost: costEvidenceForCandidate({
152
+ provider: candidate.provider,
153
+ model: candidate.model,
154
+ limitUsd: profile.limits.maxEstimatedCostUsd,
155
+ }),
156
+ ...(compatibility ? { compatibility } : {}),
157
+ };
158
+ });
159
+ }