@bitkyc08/opencodex 2.60.0 → 2.61.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 (249) hide show
  1. package/AGENTS_INSTALL.md +64 -0
  2. package/README.md +28 -1
  3. package/bin/ocx.mjs +382 -209
  4. package/gui/dist/assets/App-CH6C5H7x.js +50 -0
  5. package/gui/dist/assets/Tray-CLZh48fM.js +1 -0
  6. package/gui/dist/assets/index-_bpvxJu0.css +1 -0
  7. package/gui/dist/assets/index-wpTOyepx.js +86 -0
  8. package/gui/dist/assets/usage-companion-chart-a0N58rRI.js +1 -0
  9. package/gui/dist/favicon.png +0 -0
  10. package/gui/dist/index.html +2 -2
  11. package/gui/dist/provider-icons/stepfun-color.svg +1 -0
  12. package/package.json +5 -1
  13. package/src/adapters/anthropic.ts +16 -0
  14. package/src/adapters/coding-agent/protocol.ts +36 -6
  15. package/src/adapters/coding-agent/turn.ts +10 -2
  16. package/src/adapters/command-code.ts +2 -1
  17. package/src/adapters/cursor/catalog.ts +51 -7
  18. package/src/adapters/cursor/protobuf-request.ts +6 -3
  19. package/src/adapters/cursor/request-builder.ts +13 -3
  20. package/src/adapters/cursor.ts +11 -2
  21. package/src/adapters/declaration-carrier.ts +45 -0
  22. package/src/adapters/devin.ts +75 -23
  23. package/src/adapters/google-antigravity-wire.ts +5 -2
  24. package/src/adapters/google-errors.ts +7 -1
  25. package/src/adapters/google.ts +29 -5
  26. package/src/adapters/image.ts +4 -1
  27. package/src/adapters/input-media-guard.ts +21 -9
  28. package/src/adapters/kiro/usage.ts +3 -2
  29. package/src/adapters/kiro-tool-fallback.ts +1 -1
  30. package/src/adapters/ollama-native.ts +6 -0
  31. package/src/adapters/openai-chat/developer-role.ts +61 -0
  32. package/src/adapters/openai-chat/messages.ts +46 -27
  33. package/src/adapters/openai-chat/parallel-tool-calls.ts +32 -0
  34. package/src/adapters/openai-chat/passthrough.ts +33 -9
  35. package/src/adapters/openai-chat/reasoning-wire.ts +89 -0
  36. package/src/adapters/openai-chat.ts +18 -57
  37. package/src/adapters/openai-responses/passthrough.ts +2 -0
  38. package/src/adapters/registry.ts +3 -2
  39. package/src/adapters/run-turn-queue.ts +178 -29
  40. package/src/adapters/xai-web-search.ts +16 -1
  41. package/src/bridge/errors.ts +8 -2
  42. package/src/bridge/response-json.ts +9 -1
  43. package/src/bridge/sse.ts +10 -0
  44. package/src/chat/inbound.ts +141 -5
  45. package/src/claude/desktop-3p.ts +7 -1
  46. package/src/claude/desktop-first-party.ts +183 -0
  47. package/src/claude/desktop-gateway-state.ts +41 -0
  48. package/src/claude/inbound-content-options.ts +6 -0
  49. package/src/claude/inbound.ts +32 -6
  50. package/src/claude/intercept/connect-proxy.ts +179 -0
  51. package/src/claude/intercept/listener.ts +122 -0
  52. package/src/claude/intercept/local-ca.ts +298 -0
  53. package/src/claude/intercept/runtime.ts +98 -0
  54. package/src/claude/intercept/settings.ts +189 -0
  55. package/src/cli/access.ts +87 -0
  56. package/src/cli/account-auth.ts +19 -0
  57. package/src/cli/capabilities.ts +31 -0
  58. package/src/cli/claude-desktop.ts +206 -16
  59. package/src/cli/codex-shim-autorestore.ts +3 -0
  60. package/src/cli/companion.ts +56 -0
  61. package/src/cli/dispatch.ts +43 -4
  62. package/src/cli/ensure-desired-integrations.ts +43 -5
  63. package/src/cli/help.ts +7 -9
  64. package/src/cli/index.ts +200 -61
  65. package/src/cli/init.ts +8 -0
  66. package/src/cli/integrations.ts +7 -1
  67. package/src/cli/registry.ts +41 -2
  68. package/src/cli/resolve.ts +230 -0
  69. package/src/cli/root.ts +24 -1
  70. package/src/cli/start-ownership-publication.ts +56 -0
  71. package/src/cli/status-probes.ts +2 -18
  72. package/src/cli/status.ts +62 -0
  73. package/src/cli/stop-report.ts +143 -0
  74. package/src/cli/uninstall-plan.ts +9 -0
  75. package/src/client/machine-listener.ts +2 -5
  76. package/src/clients/aside-profiles.ts +4 -0
  77. package/src/clients/config-export/zcode-store.ts +157 -0
  78. package/src/clients/config-export.ts +36 -0
  79. package/src/codex/app-server-processes.ts +72 -40
  80. package/src/codex/auth-api/login-flow.ts +6 -1
  81. package/src/codex/autostart-health.ts +28 -0
  82. package/src/codex/catalog/build-entries.ts +2 -2
  83. package/src/codex/catalog/effort.ts +3 -3
  84. package/src/codex/catalog/provider-models.ts +24 -15
  85. package/src/codex/catalog/retained-sync.ts +2 -2
  86. package/src/codex/convergence.ts +2 -2
  87. package/src/codex/history-provider.ts +12 -1
  88. package/src/codex/inject/config-toml.ts +41 -6
  89. package/src/codex/inject/paginated-openai-compat.ts +90 -0
  90. package/src/codex/inject.ts +18 -15
  91. package/src/codex/injected-marker.ts +18 -0
  92. package/src/codex/main-account.ts +6 -0
  93. package/src/codex/model-cache.ts +52 -6
  94. package/src/codex/model-entitlement-admission.ts +59 -0
  95. package/src/codex/model-entitlements.ts +87 -44
  96. package/src/codex/native-main-admission.ts +83 -0
  97. package/src/codex/routing/health-store.ts +39 -0
  98. package/src/codex/routing/selection.ts +37 -1
  99. package/src/codex/routing.ts +5 -41
  100. package/src/codex/shim-templates.ts +29 -3
  101. package/src/companion/settings.ts +132 -0
  102. package/src/config/atomic-write.ts +117 -5
  103. package/src/config/load-degrade.ts +34 -7
  104. package/src/config/process-state.ts +1 -1
  105. package/src/config/schema/config-schema.ts +27 -1
  106. package/src/config/schema/leaf-validators.ts +47 -0
  107. package/src/config.ts +1 -1
  108. package/src/generated/compatibility-version.json +418 -174
  109. package/src/integrations/config-io.ts +44 -10
  110. package/src/integrations/merge.ts +120 -13
  111. package/src/integrations/mutation-plan.ts +124 -18
  112. package/src/integrations/registry.ts +38 -0
  113. package/src/integrations/state.ts +78 -45
  114. package/src/integrations/target.ts +208 -0
  115. package/src/integrations/writer.ts +49 -11
  116. package/src/lab/conformance/fixture-provider.ts +5 -0
  117. package/src/lib/browser-launch-notice.ts +59 -0
  118. package/src/lib/bun-runtime.ts +6 -2
  119. package/src/lib/debug.ts +40 -0
  120. package/src/lib/open-url.ts +51 -7
  121. package/src/lib/package-tree-integrity.ts +2 -1
  122. package/src/lib/package-version.ts +8 -0
  123. package/src/lib/provider-egress.ts +310 -0
  124. package/src/lib/provider-outbound.ts +59 -14
  125. package/src/lib/proxy-env.ts +82 -7
  126. package/src/lib/request-execution-budget.ts +72 -0
  127. package/src/lib/request-failure-attribution.ts +183 -0
  128. package/src/lib/request-failure-model.ts +236 -0
  129. package/src/lib/request-resend-gate.ts +138 -0
  130. package/src/lib/standalone.ts +16 -0
  131. package/src/lib/upstream-retry.ts +167 -16
  132. package/src/lib/winsw.ts +2 -2
  133. package/src/oauth/index.ts +24 -1
  134. package/src/oauth/login-cli.ts +80 -29
  135. package/src/providers/api-key-resolve.ts +133 -0
  136. package/src/providers/api-key-selection.ts +5 -1
  137. package/src/providers/key-failover.ts +31 -1
  138. package/src/providers/key-store.ts +34 -110
  139. package/src/providers/model-rename-fields.ts +147 -0
  140. package/src/providers/model-rename-migration.ts +124 -37
  141. package/src/providers/quota/vendor-probes-key.ts +37 -22
  142. package/src/providers/reasoning-metadata.ts +43 -18
  143. package/src/providers/registry/entries-core.ts +9 -4
  144. package/src/providers/registry/entries-extended.ts +29 -4
  145. package/src/providers/registry/model-seeds.ts +47 -10
  146. package/src/providers/xai-transport.ts +12 -1
  147. package/src/reasoning-effort.ts +8 -0
  148. package/src/responses/function-call-compat.ts +38 -1
  149. package/src/responses/inline-document.ts +65 -0
  150. package/src/responses/input-media.ts +42 -8
  151. package/src/responses/muse-tool-name-alias.ts +19 -0
  152. package/src/responses/parser-content.ts +8 -2
  153. package/src/responses/parser-tools.ts +3 -0
  154. package/src/responses/parser.ts +3 -1
  155. package/src/responses/schema.ts +3 -0
  156. package/src/router.ts +17 -2
  157. package/src/server/admission-model-scope.ts +219 -0
  158. package/src/server/audio-live.ts +9 -3
  159. package/src/server/audio-upstream.ts +18 -0
  160. package/src/server/auth-cors.ts +26 -0
  161. package/src/server/chat-completions.ts +55 -2
  162. package/src/server/chat-native.ts +19 -4
  163. package/src/server/claude-messages.ts +55 -17
  164. package/src/server/grok-responses-snapshot-repair.ts +113 -11
  165. package/src/server/gui-freshness.ts +103 -0
  166. package/src/server/gui-static.ts +7 -9
  167. package/src/server/images.ts +59 -6
  168. package/src/server/index/claude-intercept-lifecycle.ts +49 -0
  169. package/src/server/index/serve-options.ts +56 -10
  170. package/src/server/index/spend-ledger-lifecycle.ts +34 -8
  171. package/src/server/index/startup-warnings.ts +24 -0
  172. package/src/server/index.ts +21 -28
  173. package/src/server/lifecycle.ts +4 -4
  174. package/src/server/live-call-bindings.ts +6 -0
  175. package/src/server/live.ts +88 -3
  176. package/src/server/management/agent-settings-routes.ts +121 -36
  177. package/src/server/management/companion-routes.ts +77 -0
  178. package/src/server/management/logs-usage-routes.ts +19 -0
  179. package/src/server/management/native-integration-routes.ts +103 -6
  180. package/src/server/management/oauth-account-routes.ts +45 -7
  181. package/src/server/management/route-registry.ts +6 -0
  182. package/src/server/management/shared.ts +18 -1
  183. package/src/server/management/usage-timeline-routes.ts +44 -0
  184. package/src/server/management-api.ts +8 -9
  185. package/src/server/proxy-liveness.ts +75 -0
  186. package/src/server/relay.ts +19 -2
  187. package/src/server/request-log-failure-attribution.ts +99 -0
  188. package/src/server/request-log.ts +114 -0
  189. package/src/server/request-metrics.ts +92 -30
  190. package/src/server/responses/codex-ws-wire.ts +34 -8
  191. package/src/server/responses/combo-stream-preflight.ts +168 -6
  192. package/src/server/responses/compact.ts +11 -0
  193. package/src/server/responses/core-opaque-recovery.ts +90 -0
  194. package/src/server/responses/fetch-helpers.ts +124 -8
  195. package/src/server/responses/input-admission.ts +10 -0
  196. package/src/server/responses/passthrough-delivery.ts +14 -1
  197. package/src/server/responses/passthrough-dispatch.ts +179 -35
  198. package/src/server/responses/passthrough-error.ts +27 -8
  199. package/src/server/responses/request-prepare.ts +42 -1
  200. package/src/server/responses/request-send-budget.ts +12 -0
  201. package/src/server/responses/request-transport.ts +24 -4
  202. package/src/server/responses/reset-replay.ts +108 -0
  203. package/src/server/responses-request-tool-scope.ts +214 -0
  204. package/src/server/responses-undeclared-tool-guard.ts +4 -1
  205. package/src/server/search.ts +25 -1
  206. package/src/server/usage-ledger-retention.ts +73 -0
  207. package/src/service/cli.ts +48 -2
  208. package/src/service/health.ts +3 -2
  209. package/src/service/install-state-contract.d.mts +27 -0
  210. package/src/service/install-state-contract.mjs +34 -0
  211. package/src/service/launchd.ts +1 -1
  212. package/src/service/orchestration.ts +2 -4
  213. package/src/service/ownership-compatibility.ts +164 -0
  214. package/src/service/ownership-mutation-lease.d.mts +32 -0
  215. package/src/service/ownership-mutation-lease.mjs +211 -0
  216. package/src/service/repair.ts +45 -1
  217. package/src/service/state-lock.ts +269 -0
  218. package/src/service/state-record.d.mts +36 -0
  219. package/src/service/state-record.mjs +138 -0
  220. package/src/service/state.ts +582 -68
  221. package/src/service/windows-taskxml.ts +11 -10
  222. package/src/service.ts +7 -3
  223. package/src/tray/windows-tray.ps1 +1 -1
  224. package/src/types/config.ts +37 -0
  225. package/src/types/provider.ts +73 -0
  226. package/src/types/request.ts +28 -2
  227. package/src/types/tools.ts +19 -0
  228. package/src/types.ts +3 -0
  229. package/src/update/index.ts +207 -63
  230. package/src/update/job.ts +9 -5
  231. package/src/update/ownership-transaction.ts +47 -0
  232. package/src/update/restart-ownership.ts +54 -0
  233. package/src/update/runtime-ownership.d.mts +40 -0
  234. package/src/update/runtime-ownership.mjs +122 -0
  235. package/src/usage/attempt-delivery.ts +198 -0
  236. package/src/usage/cache-diagnostic.ts +305 -0
  237. package/src/usage/failure-fingerprint.ts +118 -0
  238. package/src/usage/failure-projection-cache.ts +174 -0
  239. package/src/usage/failure-projection.ts +174 -0
  240. package/src/usage/ledger-retention.ts +165 -0
  241. package/src/usage/log.ts +126 -79
  242. package/src/usage/request-outcome.ts +150 -0
  243. package/src/usage/retention-contract.ts +28 -0
  244. package/src/usage/summary.ts +2 -2
  245. package/src/usage/telemetry-contract.ts +237 -0
  246. package/src/usage/timeline.ts +236 -0
  247. package/src/web-search/alpha-search.ts +21 -1
  248. package/gui/dist/assets/index-BTuCbqQd.css +0 -1
  249. package/gui/dist/assets/index-DoBVdPHP.js +0 -134
@@ -0,0 +1,157 @@
1
+ // ZCode personal provider store export.
2
+ import type { ExportContext, ManagedContribution, ManagedFragment } from "./contracts";
3
+ import { authoritativeContextWindow, inputModalitiesForClient, normalizeExportModels } from "./model-metadata";
4
+ import { OPENCODE_PROVIDER_ID, LOOPBACK_API_KEY_PLACEHOLDER } from "./constants";
5
+ import { formatSelectorConjunction } from "../../integrations/merge";
6
+
7
+ /**
8
+ * The file ZCode 3.14 and later actually read their custom providers from.
9
+ *
10
+ * `v2/config.json` — the file {@link buildZcodeContribution} writes — is still
11
+ * parsed by this client, but only by a one-shot `importLegacy` hook that runs
12
+ * when this store is missing. The client creates the store on first launch, so
13
+ * on any install that has ever run the import is already spent and a later
14
+ * write to the old file is read by nobody (#5348).
15
+ *
16
+ * The shape below is the client's own, quoted from the report that opened
17
+ * #5348: it is what the client's `importLegacyPersonalProviderConfig` produced
18
+ * when it migrated this project's block, so it is observed output rather than a
19
+ * schema this project invented. Everything the client persists that we have not
20
+ * seen it derive from our block is deliberately absent — an omitted field is a
21
+ * field the client still owns.
22
+ */
23
+ export const ZCODE_STORE_SCHEMA_VERSION = 1;
24
+
25
+ /**
26
+ * `group` places the provider among the user's own personal providers rather
27
+ * than the builtin list the client ships and rewrites. `api.type` is the same
28
+ * choice `kind: "openai"` makes in the legacy block: the OpenAI Responses
29
+ * protocol, which is the only surface this proxy speaks natively.
30
+ */
31
+ export const ZCODE_STORE_PROVIDER_GROUP = "standard-personal";
32
+ export const ZCODE_STORE_API_TYPE = "openai-responses";
33
+ export const ZCODE_STORE_PROVIDER_NAME = "OpenCodex";
34
+
35
+ /** Where the store keeps one rule per provider, and one per (provider, model). */
36
+ export const ZCODE_STORE_PROVIDER_RULES_PATH = ["config", "providerConfigRules", "providerRules"] as const;
37
+ export const ZCODE_STORE_MODEL_RULES_PATH = ["config", "modelConfigRules", "providerModelRules"] as const;
38
+
39
+ export interface ZcodeStoreProviderRule {
40
+ providerId: string;
41
+ enabled: true;
42
+ providerName: string;
43
+ config: {
44
+ group: string;
45
+ access: { apiKey: string };
46
+ api: { type: string; baseUrl: string };
47
+ personalModelIds: string[];
48
+ modelOrder: string[];
49
+ };
50
+ }
51
+
52
+ export interface ZcodeStoreModelRule {
53
+ providerId: string;
54
+ modelId: string;
55
+ config: { properties: { contextWindow: number } };
56
+ }
57
+
58
+ /**
59
+ * Is this document a store whose schema we can write?
60
+ *
61
+ * Only the one version whose shape has been observed. A store carrying any
62
+ * other `schemaVersion` — or none, or a document that is not an object — is a
63
+ * file we cannot merge into without asserting a nesting we have never seen,
64
+ * and a wrong assertion there does not fail loudly: it replaces the provider
65
+ * list the user keeps in the same file. The integration reports the write as
66
+ * ineffective in that case instead of guessing.
67
+ */
68
+ export function zcodeStoreSchemaEstablished(parsed: unknown): boolean {
69
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return false;
70
+ return (parsed as Record<string, unknown>).schemaVersion === ZCODE_STORE_SCHEMA_VERSION;
71
+ }
72
+
73
+ /**
74
+ * The selector addressing one model rule, or null when the id cannot be spelled.
75
+ *
76
+ * A rule is identified by the PAIR, so both fields are named: a selector
77
+ * carrying only the model would match another provider's rule for the same
78
+ * model. The conjunction grammar reserves `,` and `]`, and a model id holding
79
+ * either cannot be addressed unambiguously — that row ships without its
80
+ * context window rather than with a selector that points somewhere else.
81
+ */
82
+ function modelRuleSelector(modelId: string): string | null {
83
+ return formatSelectorConjunction([
84
+ { field: "providerId", value: OPENCODE_PROVIDER_ID },
85
+ { field: "modelId", value: modelId },
86
+ ]);
87
+ }
88
+
89
+ /** The rows this export publishes, filtered exactly as the legacy block filters them. */
90
+ function storeModels(ctx: ExportContext): string[] {
91
+ const ids: string[] = [];
92
+ for (const model of normalizeExportModels(ctx.models)) {
93
+ if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
94
+ ids.push(model.namespaced);
95
+ }
96
+ return ids;
97
+ }
98
+
99
+ /**
100
+ * The provider rule the client reads, built from the same context the legacy
101
+ * block is built from.
102
+ *
103
+ * `baseUrl` carries the `/v1` root for the same reason the legacy block does:
104
+ * this client appends `/responses` for the Responses protocol, so requests land
105
+ * on `/v1/responses`. The serialized credential is always the non-secret
106
+ * loopback placeholder.
107
+ */
108
+ export function buildZcodeStoreProviderRule(ctx: ExportContext): ZcodeStoreProviderRule {
109
+ const ids = storeModels(ctx);
110
+ return {
111
+ providerId: OPENCODE_PROVIDER_ID,
112
+ enabled: true,
113
+ providerName: ZCODE_STORE_PROVIDER_NAME,
114
+ config: {
115
+ group: ZCODE_STORE_PROVIDER_GROUP,
116
+ access: { apiKey: LOOPBACK_API_KEY_PLACEHOLDER },
117
+ api: { type: ZCODE_STORE_API_TYPE, baseUrl: `${ctx.baseUrl.replace(/\/v1\/?$/, "")}/v1` },
118
+ personalModelIds: ids,
119
+ /*
120
+ * The client persists the picker order separately from membership. Ours is
121
+ * the catalog order the rest of this export already sorts by, so a refresh
122
+ * that adds a model puts it where every other surface puts it.
123
+ */
124
+ modelOrder: [...ids],
125
+ },
126
+ };
127
+ }
128
+
129
+ /**
130
+ * Everything this project owns inside the store: one provider rule, plus one
131
+ * model rule per row that has an authoritative context window.
132
+ *
133
+ * A row without one ships no model rule at all, which is the same
134
+ * authoritative-window rule the legacy block follows — an emitted stand-in
135
+ * would be a guessed capability. Reasoning ladders have no counterpart here
136
+ * that this project has observed the client produce, so none is written.
137
+ */
138
+ export function buildZcodeStoreContribution(ctx: ExportContext): ManagedContribution {
139
+ const fragments: ManagedFragment[] = [{
140
+ path: [...ZCODE_STORE_PROVIDER_RULES_PATH, `[providerId=${OPENCODE_PROVIDER_ID}]`],
141
+ value: buildZcodeStoreProviderRule(ctx),
142
+ }];
143
+ for (const model of normalizeExportModels(ctx.models)) {
144
+ if (inputModalitiesForClient("pi", model.inputModalities) === null) continue;
145
+ const contextWindow = authoritativeContextWindow(model.contextWindow);
146
+ if (contextWindow === undefined) continue;
147
+ const selector = modelRuleSelector(model.namespaced);
148
+ if (selector === null) continue;
149
+ const rule: ZcodeStoreModelRule = {
150
+ providerId: OPENCODE_PROVIDER_ID,
151
+ modelId: model.namespaced,
152
+ config: { properties: { contextWindow } },
153
+ };
154
+ fragments.push({ path: [...ZCODE_STORE_MODEL_RULES_PATH, selector], value: rule });
155
+ }
156
+ return { clientId: "zcode", fragments };
157
+ }
@@ -35,6 +35,18 @@ export { OPENCODE_PROVIDER_ID, OPENCODE_CONFIG_SCHEMA, OPENCODE_API_KEY_ENV, OPE
35
35
  export { normalizeExportModels } from "./config-export/model-metadata";
36
36
  export type { OmpModelEntry, OmpProviderBlock, OmpGeneratedConfig } from "./config-export/omp";
37
37
  export type { ZcodeModelEntry, ZcodeProviderBlock, ZcodeGeneratedConfig } from "./config-export/zcode";
38
+ export type { ZcodeStoreProviderRule, ZcodeStoreModelRule } from "./config-export/zcode-store";
39
+ export {
40
+ ZCODE_STORE_SCHEMA_VERSION,
41
+ ZCODE_STORE_PROVIDER_GROUP,
42
+ ZCODE_STORE_API_TYPE,
43
+ ZCODE_STORE_PROVIDER_NAME,
44
+ ZCODE_STORE_PROVIDER_RULES_PATH,
45
+ ZCODE_STORE_MODEL_RULES_PATH,
46
+ buildZcodeStoreProviderRule,
47
+ buildZcodeStoreContribution,
48
+ zcodeStoreSchemaEstablished,
49
+ } from "./config-export/zcode-store";
38
50
  export type { DshReasoningEffort, DshWireReasoningEffort, DshModelEntry, DshProviderBlock, DshGeneratedConfig } from "./config-export/dsh";
39
51
  export type { McodeProviderBlock, McodeModelEntry, McodeGeneratedConfig } from "./config-export/mcode";
40
52
  export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProviderEntry, RaycastGeneratedConfig } from "./config-export/raycast";
@@ -447,6 +459,30 @@ export function zcodeConfigPath(env: OpencodeLaunchEnv = process.env, home: stri
447
459
  return join(zcodeHomeDir(env, home), "v2", "config.json");
448
460
  }
449
461
 
462
+ /**
463
+ * The provider store a current ZCode reads, which is NOT the file above.
464
+ *
465
+ * ZCode 3.14 moved custom providers to `v2/provider_config.json` and left
466
+ * `v2/config.json` reachable only through a one-shot import that runs when the
467
+ * new file is missing. The client creates the new file on first launch, so on
468
+ * an install that has ever run, the import has already happened and never runs
469
+ * again — every later write to `v2/config.json` is read by nobody (#5348).
470
+ *
471
+ * This project does not write this file; it names it so the integration can
472
+ * tell whether its own write can still reach the client. The env override is
473
+ * ZCode's own (`ZCODE_PERSONAL_PROVIDER_CONFIG_FILE`), so an operator who
474
+ * relocated the store is measured against the file their client actually opens
475
+ * rather than the default location. A relative override is refused for the same
476
+ * reason `ZCODE_DATA_DIR` refuses one: we and the client would disagree about
477
+ * which file it names, and here that disagreement decides whether an apply is
478
+ * reported as effective.
479
+ */
480
+ export function zcodeProviderStorePath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
481
+ const override = env.ZCODE_PERSONAL_PROVIDER_CONFIG_FILE?.trim();
482
+ if (override) return absoluteClientPath(override, home, "ZCODE_PERSONAL_PROVIDER_CONFIG_FILE");
483
+ return join(zcodeHomeDir(env, home), "v2", "provider_config.json");
484
+ }
485
+
450
486
  /**
451
487
  * Prime Agent resolves its agent directory from `PRIME_AGENT_CODING_AGENT_DIR`
452
488
  * — the brand-derived spelling of the `PI_CODING_AGENT_DIR` that `ompAgentDir`
@@ -786,6 +786,58 @@ function catalogStatusFromProcesses(
786
786
  return { state: stale ? "stale" : "fresh", processes: withStarts, catalogMtimeMs };
787
787
  }
788
788
 
789
+ interface ComputedCodexAppServerCatalogStatus {
790
+ status: CodexAppServerCatalogStatus;
791
+ processes: CodexAppServerProcess[];
792
+ }
793
+
794
+ /**
795
+ * Compute one catalog-state observation while retaining the command lines from the
796
+ * same process enumeration. The public collector deliberately exposes only the
797
+ * identity and timestamp projection; post-write warning code also needs the matched
798
+ * process records, and re-enumerating there creates a race between classification and
799
+ * reporting (as well as a second expensive Windows CIM walk).
800
+ */
801
+ function computeCodexAppServerCatalogStatus(
802
+ io: CodexAppServerProcessIo,
803
+ ): ComputedCodexAppServerCatalogStatus {
804
+ const platform = io.platform ?? process.platform;
805
+ const getuid = io.getuid ?? (() => {
806
+ try {
807
+ return typeof process.getuid === "function" ? process.getuid() : undefined;
808
+ } catch {
809
+ return undefined;
810
+ }
811
+ });
812
+ let snapshots: ProcessSnapshot[];
813
+ let enumerationFailed = false;
814
+ const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
815
+ try {
816
+ snapshots = enumerate();
817
+ } catch {
818
+ // A failed process read is unknown, never proof that nothing is running.
819
+ snapshots = [];
820
+ enumerationFailed = true;
821
+ }
822
+ const processes = codexAppServerProcessesFromSnapshots(snapshots);
823
+ if (processes.length === 0) {
824
+ return {
825
+ processes,
826
+ status: enumerationFailed
827
+ ? { state: "unknown", processes: [], catalogMtimeMs: null }
828
+ : { state: "not_running", processes: [], catalogMtimeMs: null },
829
+ };
830
+ }
831
+ const catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
832
+ const starts = io.readStartMs
833
+ ? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
834
+ : readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
835
+ return {
836
+ processes,
837
+ status: catalogStatusFromProcesses(processes, catalogMtimeMs, starts),
838
+ };
839
+ }
840
+
789
841
  // Short TTL: process listing + stat run once per window even under per-turn
790
842
  // guidance calls (#857).
791
843
  let catalogStateCache: { atMs: number; status: CodexAppServerCatalogStatus } | null = null;
@@ -889,41 +941,7 @@ export function collectCodexAppServerCatalogState(
889
941
  && now - catalogStateCache.atMs < catalogStateTtlMs(catalogStateCache.status.state)) {
890
942
  return catalogStateCache.status;
891
943
  }
892
- const compute = (): CodexAppServerCatalogStatus => {
893
- const platform = io.platform ?? process.platform;
894
- const getuid = io.getuid ?? (() => {
895
- try {
896
- return typeof process.getuid === "function" ? process.getuid() : undefined;
897
- } catch {
898
- return undefined;
899
- }
900
- });
901
- let snapshots: ProcessSnapshot[];
902
- let enumerationFailed = false;
903
- const enumerate = io.listSnapshots ?? (() => defaultListSnapshots(platform, getuid));
904
- try {
905
- snapshots = enumerate();
906
- } catch {
907
- // Enumeration failure must never read as "nothing running" — that would let
908
- // positive model guidance through on guesswork (#857). The injected seam gets
909
- // the same contract as the default path: whoever enumerates, a failure to read
910
- // the process list is unknown, not an empty machine.
911
- snapshots = [];
912
- enumerationFailed = true;
913
- }
914
- const processes = codexAppServerProcessesFromSnapshots(snapshots);
915
- if (processes.length === 0) {
916
- return enumerationFailed
917
- ? { state: "unknown", processes: [], catalogMtimeMs: null }
918
- : { state: "not_running", processes: [], catalogMtimeMs: null };
919
- }
920
- const catalogMtimeMs = (io.catalogMtimeMs ?? defaultCatalogMtimeMs)();
921
- const starts = io.readStartMs
922
- ? new Map(processes.map(proc => [proc.pid, io.readStartMs!(proc.pid)] as const))
923
- : readProcessStartMsBatch(processes.map(proc => proc.pid), platform);
924
- return catalogStatusFromProcesses(processes, catalogMtimeMs, starts);
925
- };
926
- const status = compute();
944
+ const status = computeCodexAppServerCatalogStatus(io).status;
927
945
  if (fullyDefault) {
928
946
  catalogStateCache = { atMs: now, status };
929
947
  }
@@ -1228,16 +1246,30 @@ export function afterCatalogWriteHandleAppServers(
1228
1246
  options: AfterCatalogWriteAppServerOptions,
1229
1247
  ): AfterCatalogWriteAppServerResult {
1230
1248
  const excluded = new Set(options.excludePids ?? []);
1249
+ const hint = STALE_CODEX_APP_SERVER_HINT;
1250
+ if (!options.restart) {
1251
+ // A running process is not necessarily stale. Classify the exact process
1252
+ // enumeration that supplies the warning, and stay quiet when freshness cannot be
1253
+ // established rather than presenting an unknown observation as a known mismatch.
1254
+ const observed = computeCodexAppServerCatalogStatus(options.io ?? {});
1255
+ const starts = new Map(observed.status.processes.map(process => [process.pid, process.startedAtMs]));
1256
+ const catalogMtimeMs = observed.status.catalogMtimeMs;
1257
+ const processes = observed.processes.filter(process => !excluded.has(process.pid));
1258
+ const staleProcesses = catalogMtimeMs === null
1259
+ ? []
1260
+ : processes.filter(process => {
1261
+ const startedAtMs = starts.get(process.pid);
1262
+ return startedAtMs !== null && startedAtMs !== undefined && startedAtMs <= catalogMtimeMs;
1263
+ });
1264
+ if (staleProcesses.length === 0) return { processes, warned: false, hint };
1265
+ options.log?.error(formatStaleCodexAppServerWarning(staleProcesses));
1266
+ return { processes: staleProcesses, warned: true, hint };
1267
+ }
1231
1268
  const processes = listCodexAppServerProcesses(options.io)
1232
1269
  .filter(process => !excluded.has(process.pid));
1233
- const hint = STALE_CODEX_APP_SERVER_HINT;
1234
1270
  if (processes.length === 0) {
1235
1271
  return { processes, warned: false, hint };
1236
1272
  }
1237
- if (!options.restart) {
1238
- options.log?.error(formatStaleCodexAppServerWarning(processes));
1239
- return { processes, warned: true, hint };
1240
- }
1241
1273
  options.log?.log(
1242
1274
  `Stopping Codex app-server process(es): ${processes.map(process => process.pid).join(", ")} `
1243
1275
  + "(active turns may be interrupted).",
@@ -226,9 +226,12 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
226
226
  // machine. Opening it on the hub host is useless at best, and on a
227
227
  // headless host it fails. `deviceCode` is the same signal the generic
228
228
  // OAuth login route uses to make this decision.
229
+ // Reported to the caller rather than discarded (#5261): a login whose browser never opened
230
+ // is indistinguishable from one that did, so it reads as success while nothing happens.
231
+ let browserLaunch: "started" | "failed" | "skipped" = "skipped";
229
232
  if (result.url && !result.deviceCode && shouldOpenBrowserForLogin(body.openBrowser, runtimeConfig)) {
230
233
  const { openUrl } = await import("../../lib/open-url");
231
- openUrl(result.url);
234
+ browserLaunch = (await openUrl(result.url)).status === "started" ? "started" : "failed";
232
235
  }
233
236
 
234
237
  (async () => {
@@ -488,6 +491,8 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
488
491
  flowId,
489
492
  url: result.url,
490
493
  instructions: result.instructions,
494
+ // Never fatal: the URL is still a valid thing to open by hand, and the flow stays live.
495
+ browserLaunch,
491
496
  // Dropped before #3366: every device-code surface renders this field,
492
497
  // so withholding it left the GUI and CLI with no code to show.
493
498
  ...(result.deviceCode ? { deviceCode: result.deviceCode } : {}),
@@ -154,6 +154,34 @@ export function startupHealthSummary(health: StartupHealth): string {
154
154
  return action ? `${summary}; ${action}` : summary;
155
155
  }
156
156
 
157
+ /**
158
+ * What to say at the end of setup, once routing is on disk and the autostart choice is made.
159
+ *
160
+ * #5261: applying the Codex integration does not install a background service, and on Windows
161
+ * the scheduled task that a separate install would create is logon-triggered rather than
162
+ * boot-triggered. So "routing written, nothing listening" is an ordinary state after a restart
163
+ * rather than a corruption — and the user is never told, because setup ends on a success line.
164
+ *
165
+ * A boot trigger is not the missing piece and would be a false reassurance. The task runs as
166
+ * the interactive user, so before logon there is no session for it to run in; making it truly
167
+ * pre-logon means a different principal and a different service backend, not another trigger.
168
+ * What is actually missing is that nobody says the dependency exists, which is cheap to fix and
169
+ * true on every platform.
170
+ *
171
+ * Reuses the existing health model rather than re-deriving the condition, so this cannot drift
172
+ * from what `ocx status` and `ocx doctor` report about the same install.
173
+ */
174
+ export function injectedRoutingRestartWarningLines(health: StartupHealth): string[] {
175
+ if (health.status !== "at-risk") return [];
176
+ return [
177
+ // Deliberately not "nothing will restart the proxy": a healthy launcher shim does restart it,
178
+ // for CLI launches only, and is still at-risk. The summary line below says which case this is.
179
+ "⚠️ Codex routing is written to disk and survives a restart; keeping the proxy running is a separate matter.",
180
+ ` ${startupHealthSummary(health)}`,
181
+ " While the proxy is down Codex cannot sign in or reach a model. 'ocx restore' undoes the routing without needing it.",
182
+ ];
183
+ }
184
+
157
185
  function classifyStartupHealthSummary(health: StartupHealth): string {
158
186
  if (health.status === "native") return health.routingKind === "custom-remote"
159
187
  ? "custom remote Codex routing (no local restart dependency)"
@@ -1,4 +1,4 @@
1
- import { CODEX_REASONING_LEVELS } from "../../reasoning-effort";
1
+ import { CODEX_REASONING_LEVELS, type CodexReasoningLevel } from "../../reasoning-effort";
2
2
  import { clearModelCache } from "../model-cache";
3
3
  import { routedSlug, slugEquivalenceKey } from "../../providers/slug-codec";
4
4
  import { COMBO_NAMESPACE } from "../../combos";
@@ -872,7 +872,7 @@ export function mergeCatalogEntriesFromObservedState({
872
872
  // (luna: no ultra) stay intact.
873
873
  if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
874
874
  const levels = Array.isArray(e.supported_reasoning_levels)
875
- ? e.supported_reasoning_levels as Array<{ effort?: string }>
875
+ ? e.supported_reasoning_levels as Array<Partial<CodexReasoningLevel>>
876
876
  : [];
877
877
  if (levels.length > 0
878
878
  && !suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
@@ -8,7 +8,7 @@ import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCa
8
8
  import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
9
9
  import type { OcxConfig, OcxProviderConfig } from "../../types";
10
10
  import { modelInList } from "../../types";
11
- import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
11
+ import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts, type CodexReasoningLevel } from "../../reasoning-effort";
12
12
  import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
@@ -286,7 +286,7 @@ export function isGpt56NativeSlug(slug: string): boolean {
286
286
 
287
287
  export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
288
288
  const levels = Array.isArray(entry.supported_reasoning_levels)
289
- ? entry.supported_reasoning_levels as Array<{ effort?: string }>
289
+ ? entry.supported_reasoning_levels as Array<Partial<CodexReasoningLevel>>
290
290
  : [];
291
291
  const out = [...levels];
292
292
  // max is a real native rung on the 5.6 family — always restored; ultra always advertised.
@@ -300,7 +300,7 @@ export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
300
300
 
301
301
  export function ensureUltraReasoningLevel(entry: RawEntry): void {
302
302
  const levels = Array.isArray(entry.supported_reasoning_levels)
303
- ? entry.supported_reasoning_levels as Array<{ effort?: string }>
303
+ ? entry.supported_reasoning_levels as Array<Partial<CodexReasoningLevel>>
304
304
  : [];
305
305
  if (levels.length === 0) return;
306
306
  const wanted = ["max", "ultra"];
@@ -52,7 +52,7 @@ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../
52
52
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
53
53
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
54
54
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
55
- import { recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
55
+ import { cursorLiveRosterScope, recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
56
56
  import { fetchQoderModels } from "../../adapters/qoder/live-models";
57
57
  import { resolveQoderProfile } from "../../adapters/qoder/profiles";
58
58
  import { fetchDevinUsableModels } from "../../adapters/devin/live-models";
@@ -289,15 +289,19 @@ export async function fetchProviderModelsWithAuth(
289
289
  }
290
290
  if (prov.adapter === "devin") {
291
291
  if (!apiKey) return observed(configured, "degraded");
292
- const cachedDevin = getFreshCached(name, ttlMs);
292
+ // Devin's usable-model list is entitlement-specific. Bind cache reads/writes to an
293
+ // irreversible credential fingerprint so a credential switch cannot observe another
294
+ // account's roster or stale fallback (the Qoder precedent above).
295
+ const authorityIdentity = createHash("sha256").update(apiKey).digest("hex");
296
+ const cachedDevin = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
293
297
  if (cachedDevin) {
294
298
  return observed(
295
299
  withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedDevin)),
296
300
  "authoritative",
297
301
  );
298
302
  }
299
- if (isModelsFetchCoolingDown(name)) {
300
- const cooling = getStaleCached(name);
303
+ if (isModelsFetchCoolingDown(name, undefined, undefined, authorityIdentity)) {
304
+ const cooling = getStaleCached(name, authorityIdentity);
301
305
  return observed(
302
306
  withConfiguredRetention(
303
307
  cooling ? applyConfigHintsToCachedModels(name, prov, cooling) : configured,
@@ -339,17 +343,17 @@ export async function fetchProviderModelsWithAuth(
339
343
  } as CatalogModel;
340
344
  });
341
345
  const forCache = withConfiguredRetention(result, { retainComboTargets: false });
342
- if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
346
+ if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
343
347
  return observed(withConfiguredRetention(configured), "degraded");
344
348
  }
345
349
  markProviderDiscoveryOk(name, liveResult.models.length);
346
350
  return observed(withConfiguredRetention(forCache), "authoritative");
347
351
  }
348
352
  if (isCurrentCacheGeneration()) {
349
- markModelsFetchFailure(name);
353
+ markModelsFetchFailure(name, undefined, authorityIdentity);
350
354
  markProviderDiscoveryFailed(name, { reason: liveResult.error === "auth" ? "provider" : "invalid_response" });
351
355
  }
352
- const stale = getStaleCached(name);
356
+ const stale = getStaleCached(name, authorityIdentity);
353
357
  return observed(
354
358
  withConfiguredRetention(stale ? applyConfigHintsToCachedModels(name, prov, stale) : configured),
355
359
  "degraded",
@@ -361,15 +365,19 @@ export async function fetchProviderModelsWithAuth(
361
365
  // variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
362
366
  // suffix) but filter the static seed to the bases the account actually has — so models not on the
363
367
  // plan (e.g. claude-fable-5) drop out instead of failing ERROR_BAD_MODEL_NAME. Fall back to the seed.
364
- const cachedCursor = getFreshCached(name, ttlMs);
368
+ // The roster is entitlement-specific, so the cache entry is bound to an irreversible
369
+ // credential fingerprint: a credential switch must not observe the previous account's
370
+ // plan roster, its stale fallback, or its failure cooldown suppression.
371
+ const authorityIdentity = createHash("sha256").update(apiKey).digest("hex");
372
+ const cachedCursor = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
365
373
  if (cachedCursor) {
366
374
  return observed(
367
375
  withConfiguredRetention(applyConfigHintsToCachedModels(name, prov, cachedCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias)),
368
376
  "authoritative",
369
377
  );
370
378
  }
371
- if (isModelsFetchCoolingDown(name)) {
372
- const cooling = getStaleCached(name);
379
+ if (isModelsFetchCoolingDown(name, undefined, undefined, authorityIdentity)) {
380
+ const cooling = getStaleCached(name, authorityIdentity);
373
381
  return observed(
374
382
  withConfiguredRetention(
375
383
  cooling ? applyConfigHintsToCachedModels(name, prov, cooling, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
@@ -394,27 +402,28 @@ export async function fetchProviderModelsWithAuth(
394
402
  // Cache the discovery-filtered roster without combo retention so a later
395
403
  // gather can re-apply the current capture's retain set on read.
396
404
  const forCache = withConfiguredRetention(result, { retainComboTargets: false });
397
- if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
405
+ if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
398
406
  return observed(withConfiguredRetention(configured), "degraded");
399
407
  }
400
408
  // Publish roster-derived state only for a discovery the cache accepted: a stale
401
409
  // in-flight capture (generation revoked by a credential/config change) must not
402
410
  // overwrite the spelling or Max-Mode evidence of the newer one.
403
- recordLiveCursorClaudeModels(liveResult.models);
411
+ const liveRosterScope = { provider: name, key: cursorLiveRosterScope(prov.baseUrl, apiKey) };
412
+ recordLiveCursorClaudeModels(liveResult.models, liveRosterScope);
404
413
  // Live Max-Mode evidence feeds the umbrella resolver's ultra gate
405
414
  // (devlog 260828_cursor_umbrella_catalog; union with static evidence).
406
- recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? []);
415
+ recordLiveCursorMaxModeModels(liveResult.maxModeModels ?? [], liveRosterScope);
407
416
  markProviderDiscoveryOk(name, liveResult.models.length);
408
417
  return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
409
418
  }
410
419
  if (isCurrentCacheGeneration()) {
411
- markModelsFetchFailure(name);
420
+ markModelsFetchFailure(name, undefined, authorityIdentity);
412
421
  markProviderDiscoveryFailed(name, { reason: "provider" });
413
422
  console.warn(
414
423
  `[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
415
424
  );
416
425
  }
417
- const staleCursor = getStaleCached(name);
426
+ const staleCursor = getStaleCached(name, authorityIdentity);
418
427
  return observed(
419
428
  withConfiguredRetention(
420
429
  staleCursor ? applyConfigHintsToCachedModels(name, prov, staleCursor, undefined, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
@@ -15,9 +15,9 @@ import {
15
15
  availableAccountGatedNativeModels,
16
16
  codexModelEntitlementStateForAccount,
17
17
  isCodexModelEntitlementSnapshotCurrent,
18
- resolveCodexModelEntitlements,
19
18
  type CodexModelEntitlementSnapshot,
20
19
  } from "../model-entitlements";
20
+ import { resolveAdmittedCodexModelEntitlements } from "../model-entitlement-admission";
21
21
  import { isAccountNeedsReauth } from "../account-runtime-state";
22
22
  import { codexRuntimeStatePath } from "../runtime";
23
23
  import {
@@ -601,7 +601,7 @@ export async function syncCatalogModels(
601
601
  comboOmissions,
602
602
  providerModelOutcomes,
603
603
  }),
604
- resolveCodexModelEntitlements(config),
604
+ resolveAdmittedCodexModelEntitlements(config),
605
605
  ]);
606
606
  const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
607
607
  // Desired state can flip OFF during the provider await above. The catalog
@@ -78,9 +78,9 @@ import {
78
78
  availableAccountGatedNativeModels,
79
79
  codexModelEntitlementStateForAccount,
80
80
  isCodexModelEntitlementSnapshotCurrent,
81
- resolveCodexModelEntitlements,
82
81
  type CodexModelEntitlementSnapshot,
83
82
  } from "./model-entitlements";
83
+ import { resolveAdmittedCodexModelEntitlements } from "./model-entitlement-admission";
84
84
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
85
85
  import { providerCodexAccountMode } from "../providers/registry";
86
86
  import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
@@ -434,7 +434,7 @@ export async function gatherCodexCatalogCandidate(
434
434
  providerModelOutcomes,
435
435
  discoveryPolicySnapshots: discoveryPolicies,
436
436
  }),
437
- resolveCodexModelEntitlements(snapshot.config),
437
+ resolveAdmittedCodexModelEntitlements(snapshot.config),
438
438
  ]);
439
439
  const processLocal = processEvidence(source);
440
440
  const sourceEvidence = sealCatalogGatherEvidenceSession(session);
@@ -335,6 +335,17 @@ class CodexHistoryIntegrityError extends Error {
335
335
  */
336
336
  export const HISTORY_RELABEL_STANDS_DOWN = "history_paginated_requires_native_writer";
337
337
 
338
+ /**
339
+ * The narrower reason: a provider-table transition found an `openai`-tagged row Codex has
340
+ * already paginated. It is not a plain stand-down, because the transition also takes the root
341
+ * `openai_base_url` out, and that combination would send the conversation to Codex's built-in
342
+ * OpenAI endpoint rather than this proxy.
343
+ *
344
+ * A constant for the same reason as the one above: `src/codex/inject/paginated-openai-compat.ts`
345
+ * decides what to do about it, and a literal repeated in two files is how the pair drifts apart.
346
+ */
347
+ export const HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE = "history_paginated_openai_requires_native_writer";
348
+
338
349
  function assertLegacyHistoryRecord(line: string): void {
339
350
  let value: unknown;
340
351
  try { value = JSON.parse(line); } catch { throw new CodexHistoryIntegrityError("history_rollout_record_invalid"); }
@@ -459,7 +470,7 @@ export function preflightCodexHistoryInjection(
459
470
  }
460
471
  assertLegacyHistoryWritable(row.rollout_path);
461
472
  }
462
- if (foundPaginatedOpenaiRow) return "history_paginated_openai_requires_native_writer";
473
+ if (foundPaginatedOpenaiRow) return HISTORY_PAGINATED_OPENAI_NEEDS_ROOT_OVERRIDE;
463
474
  return foundPaginatedRow ? HISTORY_RELABEL_STANDS_DOWN : null;
464
475
  } catch (error) {
465
476
  return error instanceof CodexHistoryIntegrityError