@bitkyc08/opencodex 2.84.0 → 2.86.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 (246) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,93 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { isLoopbackHostname, shouldInjectApiAuthHeader } from "../../codex/loopback-target";
4
+ import { formatSelectorConjunction } from "../../integrations/merge";
5
+ import { ClientPathError } from "../config-export";
6
+ import type { ExportContext, ManagedContribution, OpencodeLaunchEnv } from "./contracts";
7
+ import { LOOPBACK_API_KEY_PLACEHOLDER, OPENCODE_PROVIDER_ID } from "./constants";
8
+ import { authoritativeContextWindow, normalizeExportModels } from "./model-metadata";
9
+
10
+ /**
11
+ * Qoder's configuration directory: `QODER_CONFIG_DIR` when set, otherwise `~/.qoder`
12
+ * (https://docs.qoder.com/cli/settings-reference). Export and detection both resolve it here.
13
+ * A relative override is refused because opencodex and Qoder would anchor it to different cwds.
14
+ */
15
+ export function qoderHomeDir(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
16
+ const override = env.QODER_CONFIG_DIR?.trim();
17
+ if (!override) return join(home, ".qoder");
18
+ if (override === "~") return home;
19
+ if (override.startsWith("~/") || override.startsWith("~\\")) return join(home, override.slice(2));
20
+ if (!isAbsolute(override)) {
21
+ throw new ClientPathError(
22
+ `QODER_CONFIG_DIR must be an absolute path or start with ~; "${override}" depends on the working directory, `
23
+ + "so opencodex and Qoder would disagree about which file it names.",
24
+ );
25
+ }
26
+ return override;
27
+ }
28
+
29
+ export function qoderConfigPath(env: OpencodeLaunchEnv = process.env, home: string = homedir()): string {
30
+ return join(qoderHomeDir(env, home), "settings.json");
31
+ }
32
+
33
+ /** Qoder IDE and qodercli share this provider and custom-model catalog. */
34
+ export function buildQoderClientConfig(ctx: ExportContext) {
35
+ if (shouldInjectApiAuthHeader(ctx.config) || !isLoopbackHostname(new URL(ctx.baseUrl).hostname)) {
36
+ throw new Error("Qoder's placeholder API key requires a loopback-only listener.");
37
+ }
38
+ return qoderDocument(ctx);
39
+ }
40
+
41
+ function qoderDocument(ctx: ExportContext) {
42
+ const models = normalizeExportModels(ctx.models);
43
+ return {
44
+ providers: {
45
+ [OPENCODE_PROVIDER_ID]: {
46
+ type: "openai-compatible",
47
+ protocol: "openai",
48
+ displayName: "opencodex",
49
+ baseUrl: ctx.baseUrl,
50
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
51
+ ...(models[0] ? { defaultModel: models[0].namespaced } : {}),
52
+ models: models.map(model => ({ model: model.namespaced })),
53
+ },
54
+ },
55
+ modelConfigs: {
56
+ customModels: models.map(model => ({
57
+ provider: OPENCODE_PROVIDER_ID,
58
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
59
+ model: model.namespaced,
60
+ baseURL: ctx.baseUrl,
61
+ displayName: model.namespaced,
62
+ maxInputTokens: authoritativeContextWindow(model.contextWindow) ?? 128000,
63
+ })),
64
+ },
65
+ };
66
+ }
67
+
68
+ export type QoderGeneratedConfig = ReturnType<typeof buildQoderClientConfig>;
69
+
70
+ export function summarizeQoder(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
71
+ const models = (document as QoderGeneratedConfig | undefined)?.modelConfigs?.customModels ?? [];
72
+ return { modelCount: models.length, modelsWithoutLimits: 0 };
73
+ }
74
+
75
+ export function buildQoderContribution(ctx: ExportContext): ManagedContribution {
76
+ // The writer gates admission before applying. Classification and disable must
77
+ // still work after the operator changes the proxy to a non-loopback bind.
78
+ const doc = qoderDocument(ctx);
79
+ return {
80
+ clientId: "qoder",
81
+ fragments: [
82
+ { path: ["providers", OPENCODE_PROVIDER_ID], value: doc.providers[OPENCODE_PROVIDER_ID] },
83
+ ...doc.modelConfigs.customModels.map(model => {
84
+ const selector = formatSelectorConjunction([
85
+ { field: "provider", value: OPENCODE_PROVIDER_ID },
86
+ { field: "model", value: model.model },
87
+ ]);
88
+ if (selector === null) throw new Error("Qoder model ID cannot be represented by a managed array selector.");
89
+ return { path: ["modelConfigs", "customModels", selector], value: model };
90
+ }),
91
+ ],
92
+ };
93
+ }
@@ -24,6 +24,7 @@ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from "nod
24
24
  import { basename, dirname, isAbsolute, join, resolve, win32 } from "node:path";
25
25
  import { shouldInjectApiAuthHeader, standaloneCodexRoutingTarget } from "../codex/inject";
26
26
  import { FORMAT_MEDIA_TYPE, serializeDocument, type ConfigFormat } from "../integrations/serialize";
27
+ import { fileIO } from "../integrations/config-io";
27
28
  import { canonicalizeReasoningEfforts } from "../reasoning-effort";
28
29
  import { expandFastExportModels } from "./config-export/fast-models";
29
30
  import { probeHostname } from "../server/proxy-liveness";
@@ -81,6 +82,8 @@ import {
81
82
 
82
83
  export type { CommandCodeGeneratedConfig, CommandCodeModelEntry, CommandCodeProviderBlock };
83
84
  export { buildCommandCodeClientConfig, summarizeCommandCode, buildCommandCodeContribution, commandCodeProviderRoot };
85
+ import { buildQoderClientConfig, summarizeQoder, buildQoderContribution, qoderConfigPath } from "./config-export/qoder";
86
+ export { buildQoderClientConfig, qoderConfigPath, qoderHomeDir, type QoderGeneratedConfig } from "./config-export/qoder";
84
87
 
85
88
 
86
89
 
@@ -475,10 +478,8 @@ export type ZcodePathIO = {
475
478
  readText: (path: string) => { kind: "text"; text: string } | { kind: "missing" } | { kind: "failed"; code?: string };
476
479
  };
477
480
  const zcodePathIO: ZcodePathIO = {
478
- readText: path => {
479
- try { return { kind: "text", text: readFileSync(path, "utf8") }; }
480
- catch (error) { return (error as NodeJS.ErrnoException).code === "ENOENT" ? { kind: "missing" } : { kind: "failed" }; }
481
- },
481
+ /** Shares the integration reader's four-MiB limit and regular-file-only bootstrap contract. */
482
+ readText: path => fileIO().readText(path),
482
483
  };
483
484
  export class ZcodeSettingsPathError extends ClientPathError {}
484
485
 
@@ -1429,6 +1430,18 @@ export function clineSettingsDir(env: OpencodeLaunchEnv = process.env, home: str
1429
1430
  }
1430
1431
 
1431
1432
  export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1433
+ qoder: {
1434
+ id: "qoder",
1435
+ filename: "qoder-settings.json",
1436
+ destination: env => qoderConfigPath(env),
1437
+ apiKeyEnv: "",
1438
+ exportHint: "Qoder reads a non-secret placeholder from settings.json; loopback needs no key.",
1439
+ build: buildQoderClientConfig,
1440
+ format: "json",
1441
+ summarize: summarizeQoder,
1442
+ buildContribution: buildQoderContribution,
1443
+ loopbackOnly: true,
1444
+ },
1432
1445
  opencode: {
1433
1446
  id: "opencode",
1434
1447
  filename: "opencode.json",
@@ -25,6 +25,7 @@ import type { PoolQuotaResult } from "./pool-quota-probe";
25
25
  import { fetchMainAccountInfoAttempt, fetchMainAccountInfo, MAIN_CACHE_TTL } from "./main-account-probe";
26
26
  import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_CACHE_TTL, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
27
27
  import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
28
+ import { trackBackgroundWork } from "../../lib/background-work";
28
29
 
29
30
  let primeInFlight: Promise<void> | null = null;
30
31
  let lastPriorityFailbackPrimeAt: number | undefined;
@@ -259,7 +260,9 @@ export async function primeCodexPoolQuotas(
259
260
  && now - lastPriorityFailbackPrimeAt < CODEX_PRIORITY_FAILBACK_REFRESH_MS) return;
260
261
  lastPriorityFailbackPrimeAt = now;
261
262
  }
262
- primeInFlight = (async () => {
263
+ // Tracked because most callers do not await it, yet it holds the native-main claim
264
+ // database open; whoever removes that directory settles it first (see background-work).
265
+ primeInFlight = trackBackgroundWork(`codex-pool-quota-prime:${reason}`, (async () => {
263
266
  const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
264
267
  const stale = pool.filter(a => {
265
268
  const q = getAccountQuota(a.id);
@@ -344,7 +347,7 @@ export async function primeCodexPoolQuotas(
344
347
  if (process.env.OPENCODEX_DEBUG_QUOTA === "1") {
345
348
  console.warn(`[codex-quota] prime done (reason=${reason}, pool=${pool.length}, refreshed=${stale.length})`);
346
349
  }
347
- })().finally(() => { primeInFlight = null; });
350
+ })().finally(() => { primeInFlight = null; }));
348
351
  return primeInFlight;
349
352
  }
350
353
 
@@ -51,6 +51,7 @@ import {
51
51
  // The half-open TRANSIENT-HOLD lease (#4701). Not the quota-cooldown probe lease imported from
52
52
  // ./routing above -- different module, different domain, and a request never holds both.
53
53
  import { releaseTransientProbe } from "../routing/probe-lease";
54
+ import { trackBackgroundWork } from "../lib/background-work";
54
55
  import {
55
56
  codexConversationIdentity,
56
57
  recordCodexThreadLineage,
@@ -66,7 +67,8 @@ import {
66
67
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_RESERVE_MODEL } from "./catalog/native-models";
67
68
  import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
68
69
  import { maskAccountId } from "../lib/privacy";
69
- import { formatErrorResponse } from "../bridge";
70
+ import { formatErrorResponse, formatUsageLimitReachedResponse } from "../bridge";
71
+ import { nextCodexPoolRecoveryAt } from "./routing/pool-recovery";
70
72
  import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota, getMainPolicyQuota, parseUsageQuota, parseMainPolicyUsageQuota, setAccountQuotaFromParsed } from "./quota";
71
73
  import { codexAccountUsesCreditsAfterLimit, codexUsageLimitResetAt } from "./account-credit-use";
72
74
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
@@ -523,6 +525,11 @@ export class CodexAccountCooldownError extends Error {
523
525
 
524
526
  export class CodexMainAccountHardLockError extends CodexAccountCooldownError {
525
527
  readonly resetAt?: number;
528
+ /**
529
+ * Earliest known recovery of another stored pool account, set only when an unpinned pool
530
+ * selection reached main (#7046). A hint for the client's retry time; never routing input.
531
+ */
532
+ poolRecoveryAt?: number;
526
533
 
527
534
  constructor(resetAt?: number, thresholds = resolveMainAccountHardLockThresholds(undefined)) {
528
535
  super(MAIN_CODEX_ACCOUNT_ID, resetAt ?? 0);
@@ -935,16 +942,40 @@ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSele
935
942
  + ` (source: ${err.cooldownSource ?? "default"}).${recovery}`;
936
943
  }
937
944
 
945
+ /**
946
+ * The soonest a main hard-lock refusal is worth retrying: a sibling pool account's known
947
+ * recovery when it comes first, otherwise main's own reset (#7046). A pool hint is held to at
948
+ * least {@link MAIN_HARD_LOCK_MIN_POOL_RETRY_MS} so a hold about to lapse cannot turn the refusal
949
+ * into a tight retry loop. Null when neither time is known and still in the future.
950
+ */
951
+ export const MAIN_HARD_LOCK_MIN_POOL_RETRY_MS = 60_000;
952
+ export function mainHardLockRetryAt(err: CodexMainAccountHardLockError, now: number): number | null {
953
+ const future = (value: number | undefined): number | null =>
954
+ typeof value === "number" && Number.isFinite(value) && value > now ? value : null;
955
+ const main = future(err.resetAt);
956
+ const pool = future(err.poolRecoveryAt);
957
+ const poolRetryAt = pool === null ? null : Math.max(pool, now + MAIN_HARD_LOCK_MIN_POOL_RETRY_MS);
958
+ if (poolRetryAt !== null && (main === null || poolRetryAt < main)) return poolRetryAt;
959
+ return main;
960
+ }
961
+
938
962
  /** HTTP form of {@link cooldownErrorMessage}, carrying Retry-After for well-behaved clients. */
939
963
  export function cooldownErrorResponse(
940
964
  err: CodexAccountCooldownError,
941
965
  now = Date.now(),
942
966
  accountSelector?: string,
943
967
  ): Response {
968
+ if (err instanceof CodexMainAccountHardLockError) {
969
+ // A local policy refusal with a known horizon, not a transient throttle (#7046): Codex keeps
970
+ // a Goal active and sleeps through `rate_limit_exceeded` for the whole Retry-After, but
971
+ // stops on `usage_limit_reached` and shows the reset. The guard itself is unchanged.
972
+ const retryAt = mainHardLockRetryAt(err, now);
973
+ return formatUsageLimitReachedResponse(cooldownErrorMessage(err, accountSelector), retryAt,
974
+ retryAt === null ? null : Math.max(1, Math.ceil((retryAt - now) / 1000)));
975
+ }
944
976
  const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
945
977
  const headers = new Headers(res.headers);
946
- if (!(err instanceof CodexReserveUnavailableError)
947
- && (!(err instanceof CodexMainAccountHardLockError) || err.resetAt !== undefined)) {
978
+ if (!(err instanceof CodexReserveUnavailableError)) {
948
979
  headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
949
980
  }
950
981
  return new Response(res.body, { status: res.status, headers });
@@ -1044,11 +1075,51 @@ async function awaitMainAccountPolicyBindingSettled(
1044
1075
  if (!settled) throw new CodexMainProfileDrainingError();
1045
1076
  }
1046
1077
 
1078
+ /**
1079
+ * Resolve the credential for one Codex turn. A main hard-lock reached through unpinned pool
1080
+ * selection is annotated with the earliest known pool recovery, so the refusal can name a retry
1081
+ * time that a sibling account may meet (#7046).
1082
+ */
1047
1083
  export async function resolveCodexAuthContext(
1048
1084
  headers: Headers,
1049
1085
  config: OcxConfig,
1050
1086
  mode: CodexAccountMode,
1051
1087
  options: ResolveCodexAuthContextOptions = {},
1088
+ ): Promise<CodexAuthContext> {
1089
+ // Filled by the inner resolver with the request's own fenced model evidence, so the hint obeys it.
1090
+ const eligibility: RecoveryHintEligibility = {};
1091
+ try {
1092
+ return await resolveCodexAuthContextUnannotated(headers, config, mode, options, eligibility);
1093
+ } catch (error) {
1094
+ if (error instanceof CodexMainAccountHardLockError && mode === "pool" && options.accountId === undefined
1095
+ && options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
1096
+ && !requiresReserveAuthorization(options.codexAuthPolicy ?? config, options.modelId, options.admission)
1097
+ // An account-gated model with no resolved roster has no known eligible sibling.
1098
+ && (!options.modelId || !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
1099
+ || eligibility.modelEligibleAccountIds !== undefined)) {
1100
+ try {
1101
+ const now = Date.now();
1102
+ error.poolRecoveryAt = nextCodexPoolRecoveryAt(config, now, codexQuotaScopeForModel(options.modelId), {
1103
+ deniedAccountIds: eligibility.deniedModelAccountIds,
1104
+ eligibleAccountIds: eligibility.modelEligibleAccountIds,
1105
+ });
1106
+ } catch { /* the hint is optional; the refusal stands without it */ }
1107
+ }
1108
+ throw error;
1109
+ }
1110
+ }
1111
+
1112
+ interface RecoveryHintEligibility {
1113
+ modelEligibleAccountIds?: ReadonlySet<string>;
1114
+ deniedModelAccountIds?: ReadonlySet<string>;
1115
+ }
1116
+
1117
+ async function resolveCodexAuthContextUnannotated(
1118
+ headers: Headers,
1119
+ config: OcxConfig,
1120
+ mode: CodexAccountMode,
1121
+ options: ResolveCodexAuthContextOptions = {},
1122
+ eligibility: RecoveryHintEligibility = {},
1052
1123
  ): Promise<CodexAuthContext> {
1053
1124
  const writerGeneration = captureConfigGeneration();
1054
1125
  const policy = options.codexAuthPolicy ?? config;
@@ -1256,6 +1327,7 @@ export async function resolveCodexAuthContext(
1256
1327
  const modelEligibleAccountIds = entitledAccountIds
1257
1328
  ? new Set([...entitledAccountIds].filter(candidate => !excludeAccountIds?.has(candidate)))
1258
1329
  : undefined;
1330
+ eligibility.modelEligibleAccountIds = modelEligibleAccountIds;
1259
1331
  // #4768: the flagships stay visible and never fail closed, so this is evidence routing may
1260
1332
  // ORDER by, not evidence it may refuse on. Read synchronously from rosters discovery has
1261
1333
  // already gathered -- no upstream fetch joins the request path for the most commonly
@@ -1273,6 +1345,7 @@ export async function resolveCodexAuthContext(
1273
1345
  undefined,
1274
1346
  { excludeAccountIds },
1275
1347
  );
1348
+ eligibility.deniedModelAccountIds = deniedModelAccountIds;
1276
1349
  const selectionOptions = {
1277
1350
  // Temporary switch drain keeps the candidate until the atomic claim rejects
1278
1351
  // it. Retained recovery makes main wholly ineligible so pool routing continues.
@@ -1475,9 +1548,10 @@ export async function resolveCodexAuthContext(
1475
1548
  if (options.primeCodexPoolQuotas) {
1476
1549
  void options.primeCodexPoolQuotas(config, reason).catch(() => {});
1477
1550
  } else {
1478
- import("./auth-api")
1551
+ // Tracked from the import on: the prime registers itself only once the module resolves.
1552
+ void trackBackgroundWork(`codex-pool-quota-prime-import:${reason}`, import("./auth-api")
1479
1553
  .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, reason))
1480
- .catch(() => {});
1554
+ .catch(() => {}));
1481
1555
  }
1482
1556
  }
1483
1557
  // Snapshot (not just the deadline) so a refused request can report WHY it is cooled:
@@ -35,6 +35,7 @@ export interface StartupHealthInputs {
35
35
  shimHealthy: boolean;
36
36
  platform: NodeJS.Platform;
37
37
  diagnosticStale?: boolean;
38
+ shimActivationPending?: boolean;
38
39
  routingAdoption?: RoutingAdoptionEvidence;
39
40
  }
40
41
 
@@ -55,6 +56,8 @@ export interface StartupHealth {
55
56
  serviceConflict: boolean;
56
57
  shimInstalled: boolean;
57
58
  shimHealthy: boolean;
59
+ /** Intact PATH-overlay shim this process could not see active; activate it rather than repair it. */
60
+ shimActivationPending?: boolean;
58
61
  shimCoverage: ShimCoverage;
59
62
  serviceSupported: boolean;
60
63
  platform: NodeJS.Platform;
@@ -171,6 +174,7 @@ export function collectStartupHealth(
171
174
  serviceSupported: service.supported,
172
175
  shimInstalled: shim.installed,
173
176
  shimHealthy: shim.healthy,
177
+ ...(shim.activationPending ? { shimActivationPending: true } : {}),
174
178
  platform: process.platform,
175
179
  ...(routingAdoption ? { routingAdoption } : {}),
176
180
  });
@@ -48,7 +48,8 @@ import { NATIVE_RESERVE_MODEL } from "./native-models";
48
48
  import { isReserveCatalogProjection, type ReserveCatalogProjection } from "./reserve";
49
49
  import { deriveEntry, finishUpstreamNativeEntry, isExactComboCatalogEntry } from "./derive-entry";
50
50
  import { PICKER_ORDER_PRIORITY_BASE, SPAWN_PRIORITY_FIELD } from "./subagent-roster";
51
- import { preserveOperatorUltraFastTiers } from "./operator-tiers";
51
+ import { applyConfiguredUltraFast } from "./configured-tiers";
52
+ import { applyNativeUltraFastTier } from "./native-ultrafast";
52
53
 
53
54
  export interface ObservedCatalogEntryBuildInput {
54
55
  readonly template: RawEntry | null;
@@ -473,6 +474,8 @@ export function applyFullModelPickerOrder(entries: RawEntry[], order: readonly s
473
474
  export interface ObservedCatalogMergeInput {
474
475
  /** Preserve operator-supplied Ultra Fast only from the exact persisted routed row. */
475
476
  readonly ultraFastTier?: boolean;
477
+ /** Current captured wire declarations; disk rows and templates never populate this set. */
478
+ readonly configuredUltraFastSlugs?: ReadonlySet<string>;
476
479
  readonly catalogModels: readonly RawEntry[];
477
480
  readonly baselineCatalogModels: readonly RawEntry[];
478
481
  readonly routedEntries: readonly RawEntry[];
@@ -514,6 +517,7 @@ export interface ObservedCatalogMergeInput {
514
517
  */
515
518
  export function mergeCatalogEntriesFromObservedState({
516
519
  ultraFastTier = false,
520
+ configuredUltraFastSlugs = new Set(),
517
521
  catalogModels,
518
522
  baselineCatalogModels,
519
523
  routedEntries,
@@ -865,7 +869,7 @@ export function mergeCatalogEntriesFromObservedState({
865
869
  if (typeof entry.slug !== "string" || !entry.slug.includes("/")
866
870
  || isNativeAliasCatalogEntry(entry) || isExactComboCatalogEntry(entry, exactComboSlugs)
867
871
  || trustedAccountBoundNativeCatalogSlug(entry) !== undefined) continue;
868
- preserveOperatorUltraFastTiers(entry, operatorRows.get(entry.slug), ultraFastTier);
872
+ applyConfiguredUltraFast(entry, operatorRows.get(entry.slug), configuredUltraFastSlugs.has(entry.slug), ultraFastTier);
869
873
  }
870
874
  const finalRoutedEntrySet = new Set(finalRoutedEntries);
871
875
  const degradedPreservedCount = preservedRoutedEntries.filter(entry => {
@@ -932,6 +936,11 @@ export function mergeCatalogEntriesFromObservedState({
932
936
  );
933
937
  }
934
938
  }
939
+ // Native Astra/Sol rows (bare and account-bound) gain or lose the documented Ultrafast
940
+ // choice with the switch; aliases, combos and reserve projections keep their own tiers.
941
+ if (!reserveProjection && !exactCombo && m.owned_by !== COMBO_NAMESPACE && !isNativeAliasCatalogEntry(e)) {
942
+ applyNativeUltraFastTier(e, ultraFastTier);
943
+ }
935
944
  if (wsEnabled) e.supports_websockets = true;
936
945
  else {
937
946
  delete e.supports_websockets;
@@ -0,0 +1,50 @@
1
+ import { COMBO_NAMESPACE } from "../../combos";
2
+ import type { ResolvedFastPolicy } from "../../providers/fastwire";
3
+ import { catalogModelSlug, type CatalogModel, type RawEntry } from "./parsing";
4
+ import { preserveOperatorUltraFastTiers } from "./operator-tiers";
5
+
6
+ const MANAGED_ULTRAFAST = "opencodex_managed_ultrafast";
7
+
8
+ /** Advertise Ultra Fast only when the eligible final wire explicitly maps that exact tier. */
9
+ export function supportsConfiguredUltraFast(policy: ResolvedFastPolicy | undefined): boolean {
10
+ return policy?.eligibility === "eligible"
11
+ && policy.fastWire?.kind === "service-tier"
12
+ && policy.fastWire.canonicalToWire.ultrafast === "ultrafast";
13
+ }
14
+
15
+ /** Carry captured capability to the pure merge without treating persisted metadata as authority. */
16
+ export function configuredUltraFastSlugs(models: readonly CatalogModel[]): ReadonlySet<string> {
17
+ const declarations = new Map<string, boolean>();
18
+ for (const model of models) {
19
+ const slug = catalogModelSlug(model);
20
+ const supported = model.provider !== COMBO_NAMESPACE && model.nativeAlias !== true
21
+ && model.supportsServiceTier === true && model.supportsUltraFastTier === true;
22
+ // A conflicting duplicate cannot lend its capability to the surviving row.
23
+ declarations.set(slug, supported && declarations.get(slug) !== false);
24
+ }
25
+ return new Set([...declarations].filter(([, supported]) => supported).map(([slug]) => slug));
26
+ }
27
+
28
+ /** Apply current configuration, or retain an independently supplied exact-row operator tier. */
29
+ export function applyConfiguredUltraFast(
30
+ entry: RawEntry,
31
+ previous: RawEntry | undefined,
32
+ configured: boolean,
33
+ retainOperatorTier: boolean,
34
+ ): void {
35
+ // This marker only rejects stale generated metadata; it never authorizes a new tier.
36
+ const wasManaged = previous?.[MANAGED_ULTRAFAST] === entry.slug;
37
+ preserveOperatorUltraFastTiers(entry, wasManaged || configured ? undefined : previous, retainOperatorTier);
38
+ delete entry[MANAGED_ULTRAFAST];
39
+ if (!configured) return;
40
+ entry.service_tiers = [...(Array.isArray(entry.service_tiers) ? entry.service_tiers : []), {
41
+ id: "ultrafast",
42
+ name: "Ultra Fast",
43
+ description: "Higher speed and usage; provider access required",
44
+ }];
45
+ entry.additional_speed_tiers = [
46
+ ...(Array.isArray(entry.additional_speed_tiers) ? entry.additional_speed_tiers : []), "ultrafast",
47
+ ];
48
+ // Advertising a selectable capability does not opt the user into its increased usage.
49
+ entry[MANAGED_ULTRAFAST] = entry.slug;
50
+ }
@@ -204,6 +204,8 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
204
204
  * Two real sources exist and both are consulted here, in the same precedence the
205
205
  * writers use (`applyCatalogMetadata` runs first, the model's own fields
206
206
  * overwrite it): the `CatalogModel` and the generated jawcode metadata table.
207
+ * Vendor-ID fallback windows are estimates for client budgets, recorded separately rather
208
+ * than promoted to serving-provider assertions.
207
209
  * Reading only the model would silently drop every provider whose capabilities
208
210
  * live in that table.
209
211
  */
@@ -230,7 +232,8 @@ function stampCapabilityProvenance(entry: RawEntry, model: CatalogModel): void {
230
232
  entry.opencodex_capability_provenance = {
231
233
  provider: model.provider,
232
234
  model_id: model.id,
233
- ...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
235
+ ...(contextWindow !== undefined && !model.contextWindowInference ? { context_window: contextWindow } : {}),
236
+ ...(model.contextWindowInference ? { inferred_context_window: contextWindow, context_window_inference: { ...model.contextWindowInference } } : {}),
234
237
  ...(inputModalities !== undefined ? { input_modalities: [...inputModalities] } : {}),
235
238
  ...(Array.isArray(model.capabilities) && model.capabilities.length > 0
236
239
  ? { capabilities: [...model.capabilities] }
@@ -1,5 +1,7 @@
1
+ import { canInferVendorContext, vendorContextInference } from "./vendor-context";
1
2
  import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
2
3
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
4
+ import { supportsConfiguredUltraFast } from "./configured-tiers";
3
5
  import { execFileSync } from "node:child_process";
4
6
  import { createHash, createHmac, randomBytes } from "node:crypto";
5
7
  import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
@@ -250,6 +252,7 @@ export function configuredReasoningSummarySupport(prov: OcxProviderConfig | unde
250
252
  return modelRecordValue(prov.modelReasoningSummaryDelivery, id) !== undefined ? true : undefined;
251
253
  }
252
254
 
255
+ /** Reapply configured limits and captured wire capabilities without trusting stale discovery hints. */
253
256
  export function applyProviderConfigHints(
254
257
  name: string,
255
258
  prov: OcxProviderConfig,
@@ -309,7 +312,12 @@ export function applyProviderConfigHints(
309
312
  // owns catalog publication, including an explicit provider-level false.
310
313
  const supportsServiceTier = serviceTierSupportFromPolicy(fastPolicy);
311
314
  const {
315
+ contextWindowInference: _staleContextInference,
316
+ contextWindow: _previousContextWindow,
317
+ contextCap: previousContextCap,
318
+ contextCapped: previousContextCapped,
312
319
  supportsServiceTier: _staleServiceTier,
320
+ supportsUltraFastTier: _staleUltraFastTier,
313
321
  fastTierDescription: _staleFastTierDescription,
314
322
  providerAlias: _staleProviderAlias,
315
323
  suppressSyntheticMax: _staleSuppressSyntheticMax,
@@ -317,16 +325,23 @@ export function applyProviderConfigHints(
317
325
  } = model;
318
326
  // 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
319
327
  const discoveredWindow = githubCopilotCatalogContextWindow(name, prov, model.id,
320
- typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined);
328
+ !model.contextWindowInference && typeof model.contextWindow === "number" && model.contextWindow > 0 ? model.contextWindow : undefined);
321
329
  const projectedLimits = clampObservedModelLimits(staticPolicy.model, {
322
330
  ...(discoveredWindow !== undefined ? { contextWindow: discoveredWindow } : {}),
323
331
  ...(typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}),
324
332
  });
333
+ const azureWindow = positiveSafeInteger(azureMetadata?.contextWindow);
334
+ const inferredWindow = projectedLimits.contextWindow === undefined && azureWindow === undefined && canInferVendorContext(name, prov)
335
+ ? vendorContextInference(model.id) : undefined;
325
336
  const hintedWindow = projectedLimits.contextWindow
326
- ?? positiveSafeInteger(azureMetadata?.contextWindow)
337
+ ?? azureWindow
338
+ ?? inferredWindow?.contextWindow
327
339
  ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined);
328
340
  const hinted = {
329
341
  ...modelWithoutServiceTier,
342
+ ...(!model.contextWindowInference && previousContextCap !== undefined
343
+ ? { contextCap: previousContextCap, contextCapped: previousContextCapped } : {}),
344
+ ...(inferredWindow ? { contextWindowInference: inferredWindow } : {}),
330
345
  ...(displayName !== undefined ? { displayName } : {}),
331
346
  ...(providerAlias !== undefined ? { providerAlias } : {}),
332
347
  ...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
@@ -343,6 +358,7 @@ export function applyProviderConfigHints(
343
358
  ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
344
359
  ...(typeof supportsVerbosity === "boolean" ? { supportsVerbosity } : {}),
345
360
  ...(typeof supportsServiceTier === "boolean" ? { supportsServiceTier } : {}),
361
+ supportsUltraFastTier: supportsConfiguredUltraFast(fastPolicy),
346
362
  ...(supportsServiceTier === true && fastPolicy.fastTierDescription !== undefined
347
363
  ? { fastTierDescription: fastPolicy.fastTierDescription }
348
364
  : {}),
@@ -431,7 +447,9 @@ export function suppressedSyntheticMaxCatalogSlugs(
431
447
  }
432
448
  return slugs;
433
449
  }
434
- export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
450
+ // google-antigravity: the static lineup is an entitlement superset (Claude 5.5 and Gemini 4 Argon
451
+ // are per-account), so a live roster that omits some of it is the expected case, not drift.
452
+ export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai", "google-antigravity"]);
435
453
 
436
454
  export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
437
455
  // CodeBuddy's vendor defaults are real callable selectors — both the bundled manifests
@@ -0,0 +1,106 @@
1
+ import type { RawEntry } from "./parsing";
2
+ import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
3
+ import { NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT61_SOL_MODEL } from "./native-models";
4
+
5
+ /**
6
+ * The Ultrafast speed choice OpenAI documents for GPT-6 Astra and GPT-6.1 Sol
7
+ * (https://developers.openai.com/api/docs/guides/ultrafast-mode and
8
+ * https://learn.chatgpt.com/docs/agent-configuration/speed, checked 2026-10-13).
9
+ *
10
+ * The upstream bundled catalog lists only `priority` for both models; ChatGPT's live model
11
+ * list adds Ultrafast for eligible plans. Codex stops consulting that live list once
12
+ * `model_catalog_json` points at our generated catalog, so without this row the speed picker
13
+ * never offers Ultrafast. The `ultraFastTier` switch is the operator's statement that their
14
+ * plan is eligible; the description still says the tier requires one.
15
+ */
16
+ export const NATIVE_ULTRAFAST_SERVICE_TIER: Readonly<{ id: string; name: string; description: string }> = Object.freeze({
17
+ id: "ultrafast",
18
+ name: "Ultrafast",
19
+ description: "Lower latency, increased usage; requires an eligible plan",
20
+ });
21
+
22
+ export const NATIVE_ULTRAFAST_MODELS: ReadonlySet<string> = new Set([NATIVE_GPT6_ASTRA_MODEL, NATIVE_GPT61_SOL_MODEL]);
23
+
24
+ function isUltraFastId(value: unknown): boolean {
25
+ return typeof value === "string" && value.trim().toLowerCase() === "ultrafast";
26
+ }
27
+
28
+ function isUltraFastDescriptor(value: unknown): boolean {
29
+ return value !== null && typeof value === "object" && "id" in value && isUltraFastId((value as { id: unknown }).id);
30
+ }
31
+
32
+ /**
33
+ * Provenance marker naming the tier fields this module added to a row. Ownership is never
34
+ * inferred from a descriptor's text: an operator may have written the very same entry, and only
35
+ * a row carrying this marker has anything for the switch to withdraw.
36
+ */
37
+ export const NATIVE_ULTRAFAST_PROVENANCE_FIELD = "opencodex_generated_service_tiers";
38
+ type TierField = "service_tiers" | "additional_speed_tiers";
39
+
40
+ function generatedFields(entry: RawEntry): Set<TierField> {
41
+ const raw = entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD];
42
+ const fields = new Set<TierField>();
43
+ if (Array.isArray(raw)) {
44
+ for (const value of raw) if (value === "service_tiers" || value === "additional_speed_tiers") fields.add(value);
45
+ }
46
+ return fields;
47
+ }
48
+
49
+ function isGeneratedDescriptor(value: unknown): boolean {
50
+ if (!isUltraFastDescriptor(value)) return false;
51
+ const tier = value as Record<string, unknown>;
52
+ return Object.keys(tier).length === 3
53
+ && tier.id === NATIVE_ULTRAFAST_SERVICE_TIER.id
54
+ && tier.name === NATIVE_ULTRAFAST_SERVICE_TIER.name
55
+ && tier.description === NATIVE_ULTRAFAST_SERVICE_TIER.description;
56
+ }
57
+
58
+ /** Bare native rows and trusted account-bound clones of the two Ultrafast models. */
59
+ export function nativeUltraFastSlug(entry: RawEntry): string | undefined {
60
+ const slug = trustedAccountBoundNativeCatalogSlug(entry)
61
+ ?? (typeof entry.slug === "string" && !entry.slug.includes("/") ? entry.slug : undefined);
62
+ return slug !== undefined && NATIVE_ULTRAFAST_MODELS.has(slug) ? slug : undefined;
63
+ }
64
+
65
+ /**
66
+ * Add (enabled) or withdraw (disabled) the documented Ultrafast choice on an eligible native row.
67
+ *
68
+ * Enabling appends the descriptor to `service_tiers` and "ultrafast" to the deprecated
69
+ * `additional_speed_tiers` mirror, after the existing Fast tier and only when absent, and records
70
+ * each field it filled in the provenance marker. A repeated sync is a no-op and an
71
+ * operator-written Ultrafast entry wins. Disabling removes only what the marker names (and only
72
+ * the unedited generated descriptor), then drops the marker; rows without it are left as they
73
+ * are. The catalog default tier is never set.
74
+ */
75
+ export function applyNativeUltraFastTier(entry: RawEntry, enabled: boolean): void {
76
+ const owned = generatedFields(entry);
77
+ if (nativeUltraFastSlug(entry) === undefined) {
78
+ delete entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD];
79
+ return;
80
+ }
81
+ const tiers = Array.isArray(entry.service_tiers) ? entry.service_tiers : undefined;
82
+ const speeds = Array.isArray(entry.additional_speed_tiers) ? entry.additional_speed_tiers : undefined;
83
+ if (enabled) {
84
+ if (!tiers?.some(isUltraFastDescriptor)) {
85
+ entry.service_tiers = [...(tiers ?? []), { ...NATIVE_ULTRAFAST_SERVICE_TIER }];
86
+ owned.add("service_tiers");
87
+ }
88
+ if (!speeds?.some(isUltraFastId)) {
89
+ entry.additional_speed_tiers = [...(speeds ?? []), NATIVE_ULTRAFAST_SERVICE_TIER.id];
90
+ owned.add("additional_speed_tiers");
91
+ }
92
+ if (owned.size > 0) entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD] = [...owned].sort();
93
+ return;
94
+ }
95
+ delete entry[NATIVE_ULTRAFAST_PROVENANCE_FIELD];
96
+ if (owned.has("service_tiers") && tiers) {
97
+ const kept = tiers.filter(tier => !isGeneratedDescriptor(tier));
98
+ if (kept.length > 0) entry.service_tiers = kept;
99
+ else delete entry.service_tiers;
100
+ }
101
+ if (owned.has("additional_speed_tiers") && speeds) {
102
+ const kept = speeds.filter(speed => !isUltraFastId(speed));
103
+ if (kept.length > 0) entry.additional_speed_tiers = kept;
104
+ else delete entry.additional_speed_tiers;
105
+ }
106
+ }