@bitkyc08/opencodex 2.41.0 → 2.43.0-preview.20260906

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 (260) hide show
  1. package/AGENTS_INSTALL.md +2 -2
  2. package/README.md +31 -0
  3. package/bin/ocx.mjs +10 -1
  4. package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
  5. package/gui/dist/assets/index-VGeQEZ_v.js +112 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic-image-codec.ts +304 -0
  9. package/src/adapters/anthropic-image-normalize.ts +8 -298
  10. package/src/adapters/anthropic.ts +30 -7
  11. package/src/adapters/command-code.ts +7 -5
  12. package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
  13. package/src/adapters/cursor/images.ts +36 -6
  14. package/src/adapters/cursor/live-transport.ts +7 -2
  15. package/src/adapters/cursor/native-exec-desktop.ts +2 -15
  16. package/src/adapters/cursor/protobuf-request.ts +54 -24
  17. package/src/adapters/cursor/tool-definitions.ts +5 -670
  18. package/src/adapters/cursor/tool-guidance.ts +236 -0
  19. package/src/adapters/cursor/tool-naming.ts +252 -0
  20. package/src/adapters/cursor/tool-schemas.ts +195 -0
  21. package/src/adapters/cursor/types.ts +6 -3
  22. package/src/adapters/exec-tool-result-normalize.ts +1 -1
  23. package/src/adapters/google-errors.ts +9 -1
  24. package/src/adapters/google.ts +1 -0
  25. package/src/adapters/identity.ts +8 -2
  26. package/src/adapters/kiro-calibration.ts +181 -0
  27. package/src/adapters/kiro.ts +135 -3
  28. package/src/adapters/openai-responses.ts +259 -29
  29. package/src/adapters/responses-code-mode.ts +59 -0
  30. package/src/adapters/tool-catalog-nudge.ts +1 -1
  31. package/src/adapters/xai-schema-analysis.ts +86 -0
  32. package/src/adapters/xai-tool-schema.ts +2 -87
  33. package/src/adapters/xai-web-search.ts +1 -1
  34. package/src/bridge.ts +47 -13
  35. package/src/chat/inbound.ts +11 -3
  36. package/src/claude/inbound-content-options.ts +60 -0
  37. package/src/claude/inbound-model-options.ts +142 -0
  38. package/src/claude/inbound-records.ts +7 -0
  39. package/src/claude/inbound.ts +10 -202
  40. package/src/claude/model-info.ts +45 -0
  41. package/src/cli/account-auth.ts +49 -9
  42. package/src/cli/account-extended.ts +7 -1
  43. package/src/cli/capabilities.ts +15 -4
  44. package/src/cli/claude.ts +232 -39
  45. package/src/cli/config-command.ts +9 -1
  46. package/src/cli/dispatch.ts +5 -1
  47. package/src/cli/doctor.ts +10 -0
  48. package/src/cli/effort.ts +372 -0
  49. package/src/cli/export-command.ts +3 -9
  50. package/src/cli/help.ts +1 -0
  51. package/src/cli/index.ts +13 -0
  52. package/src/cli/init.ts +4 -0
  53. package/src/cli/model-selection-guidance.ts +30 -0
  54. package/src/cli/models-runtime.ts +3 -2
  55. package/src/cli/models.ts +8 -3
  56. package/src/cli/observe.ts +3 -1
  57. package/src/cli/opencode.ts +4 -1
  58. package/src/cli/provider-runtime.ts +65 -0
  59. package/src/cli/provider.ts +8 -0
  60. package/src/cli/registry.ts +16 -2
  61. package/src/cli/runtime-api.ts +3 -1
  62. package/src/cli/star-prompt.ts +22 -6
  63. package/src/cli/status-probes.ts +168 -0
  64. package/src/cli/status.ts +5 -168
  65. package/src/clients/config-export/constants.ts +69 -0
  66. package/src/clients/config-export/contracts.ts +154 -0
  67. package/src/clients/config-export/dsh.ts +132 -0
  68. package/src/clients/config-export/fast-models.ts +29 -0
  69. package/src/clients/config-export/mcode.ts +83 -0
  70. package/src/clients/config-export/model-metadata.ts +108 -0
  71. package/src/clients/config-export/omp.ts +104 -0
  72. package/src/clients/config-export/zcode.ts +92 -0
  73. package/src/clients/config-export.ts +18 -710
  74. package/src/codex/account-lifecycle.ts +20 -3
  75. package/src/codex/account-usability.ts +2 -0
  76. package/src/codex/auth-api.ts +272 -32
  77. package/src/codex/auth-context.ts +328 -24
  78. package/src/codex/catalog/effort.ts +44 -5
  79. package/src/codex/catalog/metadata.ts +149 -14
  80. package/src/codex/catalog/native-models.ts +116 -4
  81. package/src/codex/catalog/parsing.ts +122 -8
  82. package/src/codex/catalog/provider-fetch.ts +154 -23
  83. package/src/codex/catalog/reserve.ts +52 -0
  84. package/src/codex/catalog/sync.ts +89 -16
  85. package/src/codex/catalog.ts +1 -1
  86. package/src/codex/convergence-types.ts +1 -0
  87. package/src/codex/convergence.ts +2 -0
  88. package/src/codex/data/upstream-models.json +169 -0
  89. package/src/codex/forward-transport-headers.ts +25 -0
  90. package/src/codex/inject.ts +99 -34
  91. package/src/codex/injected-marker.ts +30 -4
  92. package/src/codex/journal.ts +14 -0
  93. package/src/codex/legacy-config-keys.ts +68 -0
  94. package/src/codex/log-guard/inspect-schema.ts +137 -0
  95. package/src/codex/log-guard/inspect.ts +2 -134
  96. package/src/codex/loopback-target.ts +54 -0
  97. package/src/codex/main-account-cache.ts +63 -1
  98. package/src/codex/main-account-hard-lock.ts +52 -0
  99. package/src/codex/main-account.ts +3 -1
  100. package/src/codex/management-convergence.ts +3 -0
  101. package/src/codex/model-entitlements.ts +54 -4
  102. package/src/codex/project-config-warnings.ts +92 -2
  103. package/src/codex/prompt-layers/encoding.ts +80 -0
  104. package/src/codex/prompt-layers/paths.ts +54 -0
  105. package/src/codex/prompt-layers/revision.ts +55 -0
  106. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  107. package/src/codex/prompt-layers/toml-read.ts +181 -0
  108. package/src/codex/prompt-layers.ts +14 -520
  109. package/src/codex/quota-auto-refresh-state.ts +16 -0
  110. package/src/codex/quota-auto-refresh.ts +219 -0
  111. package/src/codex/quota-types.ts +51 -0
  112. package/src/codex/quota.ts +252 -93
  113. package/src/codex/reserve-availability.ts +177 -0
  114. package/src/codex/routing.ts +28 -9
  115. package/src/codex/shim.ts +53 -11
  116. package/src/codex/subagent-model-fallback.ts +23 -3
  117. package/src/combos/failover.ts +125 -7
  118. package/src/combos/identifiers.ts +89 -0
  119. package/src/combos/index.ts +4 -0
  120. package/src/combos/resolve.ts +80 -9
  121. package/src/combos/types.ts +20 -93
  122. package/src/config/subagent-models.ts +24 -0
  123. package/src/config.ts +156 -13
  124. package/src/generated/compatibility-version.json +474 -178
  125. package/src/generated/model-metadata.ts +1 -1
  126. package/src/integrations/journal.ts +65 -4
  127. package/src/integrations/store.ts +5 -0
  128. package/src/lab/events/limits.ts +4 -0
  129. package/src/lib/destination-policy.ts +31 -2
  130. package/src/lib/errors.ts +39 -0
  131. package/src/lib/provider-outbound.ts +69 -3
  132. package/src/lib/proxy-env.ts +22 -0
  133. package/src/lib/redact-folding.ts +176 -0
  134. package/src/lib/redact.ts +2 -175
  135. package/src/lib/state-store-sweeper.ts +20 -6
  136. package/src/lib/token-estimate.ts +94 -27
  137. package/src/lib/windows-user-principal.ts +53 -5
  138. package/src/oauth/account-quota-rank.ts +40 -1
  139. package/src/oauth/anthropic-routing.ts +99 -3
  140. package/src/oauth/chatgpt-device.ts +187 -0
  141. package/src/oauth/chatgpt.ts +31 -4
  142. package/src/oauth/generic-account-failover.ts +36 -13
  143. package/src/oauth/index.ts +140 -29
  144. package/src/oauth/log.ts +3 -0
  145. package/src/oauth/login-cli.ts +5 -0
  146. package/src/oauth/meta-muse.ts +117 -15
  147. package/src/oauth/pool-settings-capability.ts +15 -4
  148. package/src/providers/api-keys.ts +8 -10
  149. package/src/providers/default-aliases.ts +39 -0
  150. package/src/providers/derive.ts +10 -2
  151. package/src/providers/fastwire.ts +36 -7
  152. package/src/providers/initial-model-selection-runtime.ts +90 -0
  153. package/src/providers/initial-model-selection.ts +120 -0
  154. package/src/providers/key-failover.ts +134 -54
  155. package/src/providers/key-store.ts +11 -1
  156. package/src/providers/label.ts +1 -1
  157. package/src/providers/model-discovery.ts +76 -0
  158. package/src/providers/model-rename-startup.ts +72 -8
  159. package/src/providers/muse-subscription-usage.ts +95 -0
  160. package/src/providers/openai-sidecar.ts +17 -5
  161. package/src/providers/openai-tiers-destination.ts +102 -0
  162. package/src/providers/openai-tiers.ts +2 -99
  163. package/src/providers/opencode-go-transport.ts +41 -0
  164. package/src/providers/quota-key-accounts.ts +141 -0
  165. package/src/providers/quota-types.ts +9 -0
  166. package/src/providers/quota.ts +625 -98
  167. package/src/providers/registry.ts +60 -17
  168. package/src/providers/xai-responses-opt-in.ts +31 -5
  169. package/src/quota/reset-activation.ts +81 -0
  170. package/src/quota/reset-detector.ts +305 -0
  171. package/src/quota/reset-notify-config.ts +162 -0
  172. package/src/quota/reset-observer.ts +125 -0
  173. package/src/quota/reset-poller.ts +160 -0
  174. package/src/quota/reset-seen-store.ts +385 -0
  175. package/src/quota/reset-sinks.ts +199 -0
  176. package/src/quota/window-mapping.ts +106 -0
  177. package/src/responses/apply-patch-envelope.ts +46 -0
  178. package/src/responses/code-mode-helper-compat.ts +39 -1
  179. package/src/responses/custom-tool-compat.ts +10 -4
  180. package/src/responses/hosted-tool-policy.ts +12 -4
  181. package/src/responses/parser-content.ts +133 -0
  182. package/src/responses/parser-text-format.ts +24 -0
  183. package/src/responses/parser-tools.ts +188 -0
  184. package/src/responses/parser.ts +3 -326
  185. package/src/responses/state.ts +124 -28
  186. package/src/router.ts +48 -13
  187. package/src/routing/analytics.ts +1 -0
  188. package/src/routing/capability.ts +17 -4
  189. package/src/server/auth-cors.ts +7 -1
  190. package/src/server/background-lifecycle.ts +23 -1
  191. package/src/server/chat-completions.ts +25 -3
  192. package/src/server/claude-messages.ts +62 -5
  193. package/src/server/effort-row.ts +1 -1
  194. package/src/server/fast-row.ts +295 -0
  195. package/src/server/gui-static.ts +30 -4
  196. package/src/server/index.ts +122 -28
  197. package/src/server/live.ts +18 -4
  198. package/src/server/management/agent-settings-routes.ts +2 -2
  199. package/src/server/management/combo-routes.ts +37 -9
  200. package/src/server/management/config-routes.ts +93 -2
  201. package/src/server/management/integration-routes.ts +108 -0
  202. package/src/server/management/model-routes.ts +13 -3
  203. package/src/server/management/model-rows.ts +20 -1
  204. package/src/server/management/native-integration-routes.ts +4 -1
  205. package/src/server/management/oauth-account-routes.ts +45 -10
  206. package/src/server/management/provider-routes.ts +34 -3
  207. package/src/server/management/quota-reset-routes.ts +57 -0
  208. package/src/server/management/route-registry.ts +7 -4
  209. package/src/server/management/shared.ts +19 -5
  210. package/src/server/management/system-routes.ts +3 -2
  211. package/src/server/management-api.ts +14 -2
  212. package/src/server/ports.ts +12 -2
  213. package/src/server/relay-eager.ts +38 -23
  214. package/src/server/relay.ts +4 -0
  215. package/src/server/request-log.ts +6 -0
  216. package/src/server/responses/codex-ws-correlation.ts +65 -0
  217. package/src/server/responses/codex-ws-exchange.ts +261 -0
  218. package/src/server/responses/codex-ws-metadata.ts +134 -0
  219. package/src/server/responses/codex-ws-pool.ts +162 -0
  220. package/src/server/responses/codex-ws-request.ts +87 -0
  221. package/src/server/responses/codex-ws-session.ts +93 -0
  222. package/src/server/responses/codex-ws-wire.ts +144 -0
  223. package/src/server/responses/collaboration.ts +41 -1
  224. package/src/server/responses/compact.ts +105 -12
  225. package/src/server/responses/core.ts +510 -57
  226. package/src/server/responses/empty-completion-guard.ts +4 -0
  227. package/src/server/responses/fetch-helpers.ts +10 -3
  228. package/src/server/responses/input-admission.ts +16 -9
  229. package/src/server/responses/responses-field-backfill.ts +1 -1
  230. package/src/server/responses/ws-upstream.ts +34 -318
  231. package/src/server/responses-custom-tool-repair.ts +20 -4
  232. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  233. package/src/server/safe-response-headers.ts +23 -0
  234. package/src/server/search.ts +9 -0
  235. package/src/server/subagent-models-startup.ts +27 -0
  236. package/src/server/system-env-shell.ts +238 -0
  237. package/src/server/system-env.ts +7 -234
  238. package/src/server/ws-bridge.ts +3 -25
  239. package/src/server/xai-responses-startup.ts +21 -0
  240. package/src/service-manager-probe.ts +1 -1
  241. package/src/service.ts +55 -16
  242. package/src/types/config.ts +108 -12
  243. package/src/types/provider.ts +36 -7
  244. package/src/types/request.ts +8 -0
  245. package/src/types/tools.ts +26 -1
  246. package/src/types.ts +2 -0
  247. package/src/update/notify.ts +8 -2
  248. package/src/usage/cost.ts +38 -28
  249. package/src/usage/expected-prices.ts +34 -15
  250. package/src/usage/log.ts +2 -0
  251. package/src/usage/model-identity.ts +26 -0
  252. package/src/usage/summary.ts +15 -1
  253. package/src/vision/describe.ts +6 -0
  254. package/src/vision/image-rewrite.ts +108 -0
  255. package/src/vision/index.ts +19 -306
  256. package/src/vision/plan.ts +205 -0
  257. package/src/web-search/executor.ts +6 -0
  258. package/src/web-search/index.ts +8 -1
  259. package/gui/dist/assets/index-B2YjLA-i.css +0 -1
  260. package/gui/dist/assets/index-aPup8CKb.js +0 -112
@@ -0,0 +1,295 @@
1
+ import {
2
+ accountBoundNativeOpenAiSlugsBySelector,
3
+ shouldIncludeAccountBoundNativeOpenAi,
4
+ shouldIncludeNativeOpenAi,
5
+ UPSTREAM_NATIVE_ENTRIES,
6
+ } from "../codex/catalog/metadata";
7
+ import { comboModelId, comboPublicModelId } from "../combos/types";
8
+ import { policyModelId, policyPublicModelId } from "../routing/profile";
9
+ import type { InboundWire } from "../providers/registry";
10
+ import { fastPolicyForModel } from "../providers/service-tier";
11
+ import type { OcxConfig } from "../types";
12
+ import {
13
+ isKnownId,
14
+ knownEffortRowIds,
15
+ loadDetectedCursorEffortTable,
16
+ parseEffortRowId,
17
+ parseRequestEffortRowId,
18
+ type EffortRowKnownIds,
19
+ type ParsedEffortRowId,
20
+ } from "./effort-row";
21
+
22
+ /**
23
+ * Synthetic Fast selectors: `<base-id>--fast` published on external model listings for
24
+ * models whose resolved Fast policy is eligible, so a client that can only pick a model by
25
+ * id reaches the priority service tier. The Codex app has a picker toggle for this; nothing
26
+ * else did (devlog/_plan/260904_external_fast_wire).
27
+ *
28
+ * The marker is `--fast`, not `-fast`. Terminal `-fast` is a REAL id across this catalog —
29
+ * grok-4-fast, glm-5.3-fast, gpt-5-fast, and every Cursor fast variant — so a single hyphen
30
+ * cannot tell a product apart from a tier. `--` is the same terminal separator the effort-row
31
+ * grammar relies on for the same reason.
32
+ */
33
+ const FAST_ROW_SUFFIX = "--fast";
34
+
35
+ export interface ParsedFastRowId {
36
+ baseId: string;
37
+ }
38
+
39
+ export interface ParsedSyntheticRow {
40
+ fastRow: ParsedFastRowId | null;
41
+ effortRow: ParsedEffortRowId | null;
42
+ }
43
+
44
+ export function fastRowId(baseId: string): string {
45
+ return `${baseId}${FAST_ROW_SUFFIX}`;
46
+ }
47
+
48
+ /**
49
+ * Whether a provider/model pair's resolved Fast policy may be published as a row.
50
+ *
51
+ * `eligible` alone. `unclassified` means capability is undefined, and `decideTier` makes
52
+ * `fastMode` inert there, so publishing it would advertise a tier the runtime then refuses
53
+ * to send.
54
+ */
55
+ export function fastRowEligible(
56
+ provider: Parameters<typeof fastPolicyForModel>[0],
57
+ modelId: string,
58
+ providerName?: string,
59
+ inbound: InboundWire = "responses",
60
+ ): boolean {
61
+ return fastPolicyForModel(provider, modelId, providerName, inbound).eligibility === "eligible";
62
+ }
63
+
64
+ /** Shared discovery/export policy; native rows additionally need upstream tier evidence. */
65
+ export function catalogFastRowEligible(
66
+ config: OcxConfig,
67
+ model: { provider: string; id: string; native?: boolean; supportsServiceTier?: boolean },
68
+ ): boolean {
69
+ if (config.fastRows === false) return false;
70
+ // Configured suffix-shaped IDs are real bases; live-only ones are deliberately
71
+ // refused by ingress. Discovery must not advertise a selector ingress cannot strip.
72
+ if (model.id.endsWith(FAST_ROW_SUFFIX)
73
+ && !fastRowBases(config)(model.native ? model.id : `${model.provider}/${model.id}`)) return false;
74
+ if (model.native) {
75
+ const id = model.id.slice(model.id.lastIndexOf("/") + 1);
76
+ const tiers = UPSTREAM_NATIVE_ENTRIES.get(id)?.additional_speed_tiers;
77
+ const provider = config.providers.openai;
78
+ return Array.isArray(tiers) && tiers.includes("fast") && provider !== undefined
79
+ && fastRowEligible(provider, id, "openai");
80
+ }
81
+ if (model.supportsServiceTier !== undefined) return model.supportsServiceTier === true;
82
+ const provider = config.providers[model.provider];
83
+ return provider !== undefined && fastRowEligible(provider, model.id, model.provider);
84
+ }
85
+
86
+ /**
87
+ * Bases that may carry a fast row.
88
+ *
89
+ * Deliberately a SUPERSET of what the listings publish, and deliberately not
90
+ * `knownEffortRowIds`. That set answers "which exact ids defeat the synthetic grammar"; it
91
+ * does not answer "which bases are routable". Bare natives prove the gap: the `openai`
92
+ * registry entry declares no `models` list and the default provider config declares none
93
+ * either, because they route through a family-pattern rule instead. `gpt-5.6-sol` is
94
+ * therefore absent from it, and requiring membership would publish `gpt-5.6-sol--fast` and
95
+ * then refuse to parse it.
96
+ *
97
+ * Being too permissive costs nothing here: routing still rejects a base it cannot serve, and
98
+ * the exact-id guard in `parseFastRowId` still protects real models. Being too strict breaks
99
+ * the feature.
100
+ *
101
+ * Membership must not depend on the live-model cache. An earlier version seeded this set
102
+ * from `knownEffortRowIds` alone, which reads `getStaleCached` (router.ts:125), so a
103
+ * live-only model leaving the cache silently stopped its `--fast` selector from parsing.
104
+ * The argument that its base row leaves the listing at the same time is true but
105
+ * irrelevant: `/v1/models` is discovery, not a routing allowlist, and `routeModel` still
106
+ * serves the bare base through the default provider (router.ts:791) and the qualified base
107
+ * through its configured provider (router.ts:680). So the base kept working while only the
108
+ * fast selector broke — the asymmetry this set exists to prevent.
109
+ *
110
+ * A pure Set cannot express this. Listings also publish LIVE-discovered and retained models
111
+ * that appear in no config (`provider-fetch.ts` publishes `goModels` and `retainModels`), and
112
+ * enumerating them means reading the very cache whose churn caused the original defect. So
113
+ * this returns a PREDICATE: an id is a routable base when it is a known static/config id, or
114
+ * when it is namespaced under an enabled configured provider. The second clause is
115
+ * structural, so it holds for a live-discovered model without consulting the cache, and it
116
+ * is exactly the shape `routeModel` uses to accept a qualified id (router.ts:680).
117
+ *
118
+ * A base is RECOGNIZED here and then judged by routing, which is the component that actually
119
+ * knows whether it can serve it.
120
+ */
121
+ export function fastRowBases(config: OcxConfig): (id: string) => boolean {
122
+ const bases = new Set<string>();
123
+ // Configured providers: any model the router would accept for this provider, plus the
124
+ // namespaced and alias-namespaced spellings a listing can publish. Deliberately NOT
125
+ // knownEffortRowIds, whose live-cache half makes membership time-dependent.
126
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
127
+ if (providerConfig.disabled === true) continue;
128
+ const namespaces = [providerName, providerConfig.alias].filter(
129
+ (value): value is string => typeof value === "string" && value.length > 0,
130
+ );
131
+ const declared = [
132
+ ...(providerConfig.models ?? []),
133
+ ...(providerConfig.defaultModel ? [providerConfig.defaultModel] : []),
134
+ ...Object.values(providerConfig.modelAliases ?? {}),
135
+ ...(config.customModels ?? [])
136
+ .filter(model => model.provider === providerName && model.modelId)
137
+ .map(model => model.modelId),
138
+ ];
139
+ for (const id of declared) {
140
+ bases.add(id);
141
+ for (const namespace of namespaces) bases.add(`${namespace}/${id}`);
142
+ }
143
+ }
144
+ // The STATIC upstream table, not `visibleNativeSlugs()`: that one filters by
145
+ // disabled/shadowed state and reaches the catalog cache on disk, so it would both read a
146
+ // file per parsed selector and SHRINK as runtime state changes. A base disappearing
147
+ // mid-session would strand a client still holding the id it was published.
148
+ if (shouldIncludeNativeOpenAi(config)) {
149
+ for (const slug of UPSTREAM_NATIVE_ENTRIES.keys()) bases.add(slug);
150
+ }
151
+ if (shouldIncludeAccountBoundNativeOpenAi(config)) {
152
+ // An EMPTY observed-entry list on purpose: the default argument reads the Codex models
153
+ // cache and catalog from disk. The empty form still seeds every selector with the native
154
+ // model set, and anything publishable is in UPSTREAM_NATIVE_ENTRIES anyway.
155
+ for (const [selector, slugs] of accountBoundNativeOpenAiSlugsBySelector(config, [])) {
156
+ for (const slug of slugs) bases.add(`${selector}/${slug}`);
157
+ }
158
+ }
159
+ // Namespaces whose qualified ids route, whatever the cache currently holds.
160
+ const namespaces = new Set<string>();
161
+ // Virtual rows. A combo or routing profile is published under its canonical
162
+ // `<namespace>/<id>` AND under an operator alias, which may be an arbitrary bare string
163
+ // with no namespace to vouch for it, so the structural clause below cannot reach it. A
164
+ // combo also cannot be covered by config.providers: declaring a provider named `combo`
165
+ // is rejected outright (combos/types.ts:191).
166
+ for (const [id, combo] of Object.entries(config.combos ?? {})) {
167
+ bases.add(comboModelId(id));
168
+ bases.add(comboPublicModelId(id, combo));
169
+ }
170
+ for (const [id, profile] of Object.entries(config.routingProfiles ?? {})) {
171
+ bases.add(policyModelId(id));
172
+ bases.add(policyPublicModelId(id, profile));
173
+ }
174
+ for (const [providerName, providerConfig] of Object.entries(config.providers)) {
175
+ if (providerConfig.disabled === true) continue;
176
+ namespaces.add(providerName.toLowerCase());
177
+ if (typeof providerConfig.alias === "string" && providerConfig.alias.length > 0) {
178
+ namespaces.add(providerConfig.alias.toLowerCase());
179
+ }
180
+ }
181
+ return (id: string): boolean => {
182
+ if (bases.has(id)) return true;
183
+ const slash = id.indexOf("/");
184
+ if (slash <= 0 || slash === id.length - 1) return false;
185
+ // A live-discovered or retained model is published as `<provider>/<model>` and appears in
186
+ // no config, so structural recognition is the only cache-free way to accept it.
187
+ //
188
+ // But NOT when the remainder itself ends in the marker. A real live model may legitimately
189
+ // be named `foo--fast`; its exact id is protected by the known-id guard only while the
190
+ // discovery cache still holds it, and after eviction that guard goes quiet while this
191
+ // clause would still accept `provider/foo` structurally - silently routing a DIFFERENT
192
+ // model than the client selected. Refusing the strip is the safe side: the caller then
193
+ // sends the id verbatim and routing resolves the real model, or fails honestly.
194
+ if (id.slice(slash + 1).endsWith(FAST_ROW_SUFFIX)) return false;
195
+ return namespaces.has(id.slice(0, slash).toLowerCase());
196
+ };
197
+ }
198
+
199
+ export function parseFastRowId(
200
+ id: string,
201
+ config: Pick<OcxConfig, "fastRows">,
202
+ knownIds?: EffortRowKnownIds,
203
+ routableBases?: EffortRowKnownIds,
204
+ ): ParsedFastRowId | null {
205
+ if (config.fastRows === false) return null;
206
+ if (!id.endsWith(FAST_ROW_SUFFIX)) return null;
207
+ // An exact configured/public id always beats the synthetic grammar, the same precedence
208
+ // effort rows use. An operator who really named a model `x--fast` keeps it.
209
+ if (isKnownId(knownIds, id)) return null;
210
+ const baseId = id.slice(0, -FAST_ROW_SUFFIX.length);
211
+ if (baseId.length === 0) return null;
212
+ return isKnownId(routableBases, baseId) ? { baseId } : null;
213
+ }
214
+
215
+ /**
216
+ * True when an effort-row base still carries a fast marker, i.e. the selector nested the two
217
+ * grammars. Composition is not supported, so such an id resolves to neither rather than
218
+ * silently to whichever parser ran first.
219
+ *
220
+ * Known-id guarded, so a real model named `foo--fast` keeps its legitimate `foo--fast--high`
221
+ * effort row.
222
+ */
223
+ export function effortBaseCarriesFastMarker(
224
+ baseId: string,
225
+ knownIds: EffortRowKnownIds | undefined,
226
+ ): boolean {
227
+ return baseId.endsWith(FAST_ROW_SUFFIX) && !isKnownId(knownIds, baseId);
228
+ }
229
+
230
+ /**
231
+ * Resolve one ingress selector against both synthetic grammars, returning at most one.
232
+ * Callers pass the id the client sent and never a value another parser mutated.
233
+ */
234
+ export function parseSyntheticRowId(
235
+ id: string,
236
+ config: OcxConfig,
237
+ // Claude surfaces decode the alias before the marker is unambiguous, so they pass the
238
+ // decoded form for Fast while effort parsing keeps seeing the id the client sent. A THUNK,
239
+ // not a string: arguments are evaluated before the call, so an eager decode would run its
240
+ // alias lookups even on the fastRows-off path this function exists to leave untouched.
241
+ fastSelector?: () => string,
242
+ ): ParsedSyntheticRow {
243
+ // Explicit opt-out preserves the effort-only parser and avoids Fast inventory work.
244
+ if (config.fastRows === false) {
245
+ return { fastRow: null, effortRow: parseRequestEffortRowId(id, config) };
246
+ }
247
+ const selector = fastSelector?.() ?? id;
248
+ const wantsFast = selector.endsWith(FAST_ROW_SUFFIX);
249
+ // Bail before building any inventory when neither grammar can match. A readable Claude
250
+ // alias is `claude-ocx-<provider>--<model>`, so it ALWAYS contains `--`: testing only for
251
+ // the separator would rebuild the whole model inventory on every Claude turn for a
252
+ // selector that cannot be a fast row. With effort parsing off, the terminal suffix is the
253
+ // only thing that can match.
254
+ const wantsEffort = config.cursorEffortRows === true && id.lastIndexOf("--") > 0;
255
+ if (!wantsFast && !wantsEffort) return { fastRow: null, effortRow: null };
256
+ const knownIds = knownEffortRowIds(config);
257
+ const fastRow = wantsFast
258
+ ? parseFastRowId(selector, config, knownIds, fastRowBases(config))
259
+ : null;
260
+ if (fastRow) return { fastRow, effortRow: null };
261
+ // Cursor install detection stays behind its own flag, exactly as parseRequestEffortRowId
262
+ // gates it today.
263
+ const effortRow = wantsEffort
264
+ ? parseEffortRowId(id, config, { knownIds, table: loadDetectedCursorEffortTable() })
265
+ : null;
266
+ return effortRow && effortBaseCarriesFastMarker(effortRow.baseId, knownIds)
267
+ ? { fastRow: null, effortRow: null }
268
+ : { fastRow: null, effortRow };
269
+ }
270
+
271
+ /** Fast-only resolution for surfaces that never parsed an effort row. */
272
+ export function parseFastOnlyRowId(
273
+ config: OcxConfig,
274
+ selector: () => string,
275
+ ): ParsedFastRowId | null {
276
+ if (config.fastRows === false) return null;
277
+ return parseSyntheticRowId("", config, selector).fastRow;
278
+ }
279
+
280
+ /**
281
+ * Add a fast sibling beside an eligible row. The base row is always kept: a fast row is an
282
+ * addition, never a replacement. That is the deliberate difference from `fastMode`, which
283
+ * replaces the listed Cursor id — replacement suits a global switch, but a per-request
284
+ * selector has to leave the default reachable.
285
+ */
286
+ export function expandFastRow<T extends { id: string }>(
287
+ row: T,
288
+ eligible: boolean,
289
+ config: Pick<OcxConfig, "fastRows">,
290
+ knownIds?: EffortRowKnownIds,
291
+ ): T[] {
292
+ if (config.fastRows === false || !eligible) return [row];
293
+ const id = fastRowId(row.id);
294
+ return isKnownId(knownIds, id) ? [row] : [row, { ...row, id }];
295
+ }
@@ -90,6 +90,22 @@ function runtimeRoleMeta(runtimeRole: string): string {
90
90
  return `<meta name="opencodex-runtime-role" content="${escapeHtmlAttribute(runtimeRole)}">`;
91
91
  }
92
92
 
93
+ /**
94
+ * Does this bind require a typed management credential?
95
+ *
96
+ * Emitted for the same reason as the role: so the dashboard can answer a question on first
97
+ * paint without asking a remote-hub endpoint. It is NOT the same question as the role.
98
+ * `standalone` + `hostname: "0.0.0.0"` is an operator who deliberately exposed the dashboard
99
+ * and must type the admin token, while a `hub` on loopback still mints its own session — so
100
+ * the role cannot stand in for this, and using it that way locked out exactly the operator
101
+ * who is supposed to see the prompt.
102
+ *
103
+ * Non-secret: it restates the bind the operator chose, which `/healthz` and the dashboard
104
+ * already reflect.
105
+ */
106
+ function managementAuthRequiredMeta(required: boolean): string {
107
+ return `<meta name="opencodex-management-auth-required" content="${required ? "1" : "0"}">`;
108
+ }
93
109
  function htmlDocumentResponse(html: string): Response {
94
110
  return new Response(html, {
95
111
  headers: {
@@ -101,9 +117,18 @@ function htmlDocumentResponse(html: string): Response {
101
117
  });
102
118
  }
103
119
 
104
- function htmlResponse(path: string, session?: GuiSessionBootstrap, runtimeRole?: string): Response {
120
+ function htmlResponse(
121
+ path: string,
122
+ session?: GuiSessionBootstrap,
123
+ runtimeRole?: string,
124
+ managementAuthRequired?: boolean,
125
+ ): Response {
105
126
  let html = readFileSync(path, "utf8");
106
- const bootstrap = `${runtimeRole ? runtimeRoleMeta(runtimeRole) : ""}${session ? sessionBootstrapMeta(session) : ""}`;
127
+ const bootstrap = [
128
+ runtimeRole ? runtimeRoleMeta(runtimeRole) : "",
129
+ managementAuthRequired === undefined ? "" : managementAuthRequiredMeta(managementAuthRequired),
130
+ session ? sessionBootstrapMeta(session) : "",
131
+ ].join("");
107
132
  if (bootstrap) {
108
133
  html = html.includes("</head>") ? html.replace("</head>", `${bootstrap}</head>`) : `${bootstrap}${html}`;
109
134
  }
@@ -126,6 +151,7 @@ export function serveGuiFile(
126
151
  guiDist = findGuiDist(),
127
152
  session?: GuiSessionBootstrap,
128
153
  runtimeRole?: string,
154
+ managementAuthRequired?: boolean,
129
155
  ): Response | null {
130
156
  if (!guiDist) return null;
131
157
  const filePath = resolveGuiFilePath(guiDist, pathname);
@@ -135,7 +161,7 @@ export function serveGuiFile(
135
161
  if (!extname(pathname)) {
136
162
  const indexPath = join(guiDist, "index.html");
137
163
  if (isFile(indexPath)) {
138
- return htmlResponse(indexPath, session, runtimeRole);
164
+ return htmlResponse(indexPath, session, runtimeRole, managementAuthRequired);
139
165
  }
140
166
  }
141
167
  return null;
@@ -143,7 +169,7 @@ export function serveGuiFile(
143
169
 
144
170
  const ext = extname(filePath);
145
171
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
146
- if (ext === ".html") return htmlResponse(filePath, session, runtimeRole);
172
+ if (ext === ".html") return htmlResponse(filePath, session, runtimeRole, managementAuthRequired);
147
173
  // Snapshot bytes before returning the response. Bun.file is lazy: if gui/dist is replaced
148
174
  // after Bun frames the response but before the stream finishes, its Content-Length can
149
175
  // describe the old file while the body comes from the new one (#2792).
@@ -12,7 +12,6 @@ import {
12
12
  } from "./ws-bridge";
13
13
  import type { Server, ServerWebSocket } from "bun";
14
14
  import {
15
- DEFAULT_SUBAGENT_MODELS,
16
15
  applyProxyEnv,
17
16
  armClaudeCodeBaseline,
18
17
  loadConfig,
@@ -22,6 +21,8 @@ import {
22
21
  } from "../config";
23
22
  import { grokDefaultReasoningEffort } from "../grok/effort";
24
23
  import { flushConfigDirHardening } from "../config/paths";
24
+ import { migrateStartupSubagentModels } from "./subagent-models-startup";
25
+ import { migrateStartupXaiResponses } from "./xai-responses-startup";
25
26
  import { reconcileOAuthProviders } from "../oauth";
26
27
  import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
27
28
  import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
@@ -33,8 +34,12 @@ import {
33
34
  type NativeCodexOwnership,
34
35
  type OwnershipInspection,
35
36
  } from "../integrations/native/ownership-preflight";
36
- import { createResetCreditWhamClient, registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
37
+ import {
38
+ createResetCreditWhamClient,
39
+ registerCodexCooldownRecoveryProbeWorker,
40
+ } from "../codex/auth-api";
37
41
  import { activateResetCreditAutoRedeem } from "../codex/reset-credit-auto-redeem";
42
+ import { registerCodexQuotaAutoRefreshWorker } from "../codex/quota-auto-refresh";
38
43
  import {
39
44
  reconcileLiveStateStores,
40
45
  setLiveStateStoreConfig,
@@ -230,6 +235,7 @@ import { recordCursorSeen } from "../integrations/cursor-seen";
230
235
  import { detectCursorInstalls } from "../integrations/cursor-detect";
231
236
  import { loadCursorEffortTable } from "../integrations/cursor-effort-table";
232
237
  import { expandCursorEffortRow, knownEffortRowIds } from "./effort-row";
238
+ import { catalogFastRowEligible, expandFastRow } from "./fast-row";
233
239
 
234
240
  export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
235
241
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -509,7 +515,7 @@ function attachLiveSidebandUpstream(
509
515
 
510
516
  // Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
511
517
 
512
- // Source invariant for tests/passthrough-abort.test.ts after the pure module split:
518
+ // Source invariant for tests/responses/passthrough-abort.test.ts after the pure module split:
513
519
  // if (isEventStream && upstreamResponse.body) {
514
520
  // const repairConfig = route.provider.responsesItemIdRepair;
515
521
  // const needsClientRewrite = imageGenCallAliases.size > 0
@@ -583,6 +589,8 @@ export interface StartServerDeps {
583
589
  readinessGate?: ReadinessGate;
584
590
  /** Test-only package-tree observation; production captures package.json identity at boot. */
585
591
  packageTreeIntegrity?: PackageTreeIntegrityGuard;
592
+ /** Test-only seam for observing quota-worker registration ownership. */
593
+ registerCodexQuotaAutoRefreshWorker?: typeof registerCodexQuotaAutoRefreshWorker;
586
594
  }
587
595
 
588
596
  function inspectStartupOwnership(
@@ -645,7 +653,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
645
653
  // Captured before loadConfig() starts the optional ACL flight so stop() drains the same dir
646
654
  // even if OPENCODEX_HOME changes underneath a long-lived process.
647
655
  const startupConfigDir = getConfigDir();
648
- const config = runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig())));
656
+ const startupConfig = migrateStartupSubagentModels(
657
+ runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()))),
658
+ );
659
+ // Reconcile disk-backed presets first: it replaces provider rows and must not undo
660
+ // an in-memory wire upgrade when that upgrade's persistence is temporarily unavailable.
661
+ reconcileOAuthProviders(startupConfig);
662
+ const config = migrateStartupXaiResponses(startupConfig);
649
663
  warnAgentTaskRecoveryStartup(config);
650
664
  setLiveStateStoreConfig(config);
651
665
  applyProxyEnv(config);
@@ -655,16 +669,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
655
669
  let userCostOverlayReconciler: { stop(): void } | null = null;
656
670
  // Arm synchronously before listen. A pending journal therefore makes __main__ unusable
657
671
  // before any request can resolve its physical credential, while health/management/Pool stay live.
658
- // Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
659
- // adding/dropping models reaches existing configs on start — not just fresh installs.
660
- reconcileOAuthProviders(config);
661
672
  reconcileLiveStateStores();
662
- // Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
663
- // even [], is left alone so GUI removals persist.
664
- if (config.subagentModels === undefined) {
665
- config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
666
- saveConfig(config);
667
- }
668
673
  // authMode migration (devlog 260726_claude_auth_auto/015): before "auto" existed,
669
674
  // choosing Subscription DELETED the key, so a pre-upgrade block with no authMode is
670
675
  // indistinguishable from "never chose". Pin those to subscription once so an upgrade
@@ -798,6 +803,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
798
803
  * on the direct-spawn host into a 404 (#3192). The handler still runs its own admission, so a
799
804
  * loopback caller without a ChatGPT credential is refused inside it rather than by this gate.
800
805
  *
806
+ * The standalone Images client uses the same base URL for its two POST routes. Their handler
807
+ * keeps the paid upstream behind its own admission and forward-credential checks, so admit only
808
+ * the exact methods and paths it serves (#3428).
809
+ *
801
810
  * `GET /v1/models` is on the list for a reason that is easy to miss. When catalog
802
811
  * materialization fails or finds no source, `syncCodex` warns and injects with
803
812
  * `catalogPath: null`; Codex then builds an ONLINE model manager and `model/list` refreshes
@@ -811,14 +820,25 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
811
820
  }
812
821
  if (path === "/v1/responses/compact") return req.method === "POST";
813
822
  if (path === "/v1/alpha/search") return req.method === "POST";
814
- if (path === "/v1/models") return req.method === "GET";
815
- // Standalone realtime voice sessions (codex-rs thread/realtime/start, WebSocket
816
- // transport) — a directly-spawned `codex app-server` needs these for desktop
817
- // voice the same way it needs /v1/responses. WebSocket upgrades only; plain
818
- // HTTP on these paths stays rejected.
819
- if (path === "/v1/realtime" || path === "/v1/live") {
820
- return req.headers.get("upgrade")?.toLowerCase() === "websocket";
823
+ if (path === "/v1/images/generations" || path === "/v1/images/edits") {
824
+ return req.method === "POST";
821
825
  }
826
+ if (path === "/v1/models") return req.method === "GET";
827
+ // Realtime voice — a directly-spawned `codex app-server` needs these for desktop voice
828
+ // the same way it needs /v1/responses. Two shapes, same trust model as /v1/responses:
829
+ // - standalone sessions (codex-rs thread/realtime/start, WebSocket transport):
830
+ // WebSocket upgrades on the bare /v1/realtime and /v1/live paths only;
831
+ // - WebRTC calls (desktop v3 voice): POST call-create on /v1/live or
832
+ // /v1/realtime/calls, then the sideband join as a WebSocket upgrade on the keyed
833
+ // /v1/live/{callId}, /v1/realtime/calls/{callId}, or /v1/realtime?call_id= form
834
+ // (the join reaches this listener through the injected
835
+ // experimental_realtime_ws_base_url; openai/codex #35830).
836
+ // Plain HTTP on the upgrade paths stays rejected.
837
+ const isWebSocketUpgrade = req.headers.get("upgrade")?.toLowerCase() === "websocket";
838
+ if (path === "/v1/realtime") return isWebSocketUpgrade;
839
+ if (path === "/v1/live") return isWebSocketUpgrade || req.method === "POST";
840
+ if (path === "/v1/realtime/calls") return req.method === "POST";
841
+ if (/^\/v1\/(?:live|realtime\/calls)\/[^/]+\/?$/.test(path)) return isWebSocketUpgrade;
822
842
  return false;
823
843
  }
824
844
 
@@ -1009,8 +1029,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1009
1029
  return "public";
1010
1030
  }
1011
1031
  let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
1032
+ let unregisterQuotaAutoRefresh: (() => void) | null = null;
1012
1033
  try {
1013
1034
  backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
1035
+ unregisterQuotaAutoRefresh = (deps.registerCodexQuotaAutoRefreshWorker
1036
+ ?? registerCodexQuotaAutoRefreshWorker)(config);
1014
1037
  // External `ocx config set` / direct config.json edits run in other
1015
1038
  // processes; poll the file so Logs/Usage display prices follow them live.
1016
1039
  // Started inside the guarded startup transaction so the catch below can
@@ -1262,7 +1285,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1262
1285
  // Built directly rather than through formatErrorResponse: that helper derives
1263
1286
  // `code` from the status and message via classifyError, and these two need stable,
1264
1287
  // specific codes. `catalog_not_found` in particular is what lets a caller — and
1265
- // tests/api-key-attribution.test.ts — tell "this route exists and has no catalog"
1288
+ // tests/server/api-key-attribution.test.ts — tell "this route exists and has no catalog"
1266
1289
  // apart from "this route is gone", which is the difference between admission proof
1267
1290
  // and a vacuous pass.
1268
1291
  return withCors(
@@ -1422,6 +1445,37 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1422
1445
  // Codex catalog (client_version) and the OpenAI list shape below stay byte-identical.
1423
1446
  const wantsAnthropicList = req.headers.get("anthropic-version") !== null
1424
1447
  || url.searchParams.get("flavor") === "anthropic";
1448
+ /**
1449
+ * Whether a NATIVE slug may carry a Fast sibling.
1450
+ *
1451
+ * Both halves are required. Upstream asserts the tier per model — the same
1452
+ * `additional_speed_tiers` the Codex picker's own toggle is built from — but an
1453
+ * operator capability override or the final wire resolution can still make the
1454
+ * route ineligible, and `decideTier` would then drop the tier the row advertised.
1455
+ *
1456
+ * Declared here, above the Claude discovery call, because that call reads it while
1457
+ * the raw OpenAI mapper further down does too; defining it there would leave this
1458
+ * use in its temporal dead zone.
1459
+ */
1460
+ const nativeFastEligible = (metadataId: string): boolean =>
1461
+ catalogFastRowEligible(config, { provider: OPENAI_CODEX_PROVIDER_ID, id: metadataId, native: true });
1462
+
1463
+ /**
1464
+ * Whether a routed catalog row may carry a Fast sibling.
1465
+ *
1466
+ * A combo is its own namespace with no `config.providers` entry — declaring a
1467
+ * provider named `combo` is rejected (combos/types.ts:191) — so provider lookup
1468
+ * cannot classify it. Its aggregated `supportsServiceTier` is already true only
1469
+ * when EVERY member supports the tier (aggregation.ts:201), which is the right
1470
+ * rule for a row that fans out to all of them.
1471
+ *
1472
+ * Declared beside nativeFastEligible, above the Claude discovery call that reads
1473
+ * both; defining it near the raw OpenAI mapper below would leave that use in its
1474
+ * temporal dead zone.
1475
+ */
1476
+ const catalogRowFastEligible = (m: { provider: string; id: string; supportsServiceTier?: boolean }): boolean =>
1477
+ catalogFastRowEligible(config, m);
1478
+
1425
1479
  if (wantsAnthropicList && !url.searchParams.has("client_version")) {
1426
1480
  if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
1427
1481
  // Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
@@ -1443,7 +1497,22 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1443
1497
  : idsParam === "desktop"
1444
1498
  ? "desktop3p" as const
1445
1499
  : (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
1446
- const data = buildAnthropicModelInfos(desktopNativeSlugs, goOrdered, resolveAutoContext(config.claudeCode), idStyle, activeDesktop3pAlias, nativeContextLimits(config), config.fastMode);
1500
+ const data = buildAnthropicModelInfos(
1501
+ desktopNativeSlugs,
1502
+ goOrdered,
1503
+ resolveAutoContext(config.claudeCode),
1504
+ idStyle,
1505
+ activeDesktop3pAlias,
1506
+ nativeContextLimits(config),
1507
+ config.fastMode,
1508
+ // Explicit opt-out omits the Fast predicate.
1509
+ config.fastRows !== false
1510
+ ? (model: { provider: string; id: string; supportsServiceTier?: boolean }) =>
1511
+ model.provider === "native"
1512
+ ? nativeFastEligible(model.id)
1513
+ : catalogRowFastEligible(model)
1514
+ : undefined,
1515
+ );
1447
1516
  return jsonResponse({ data }, 200, req, policy);
1448
1517
  }
1449
1518
  if (url.searchParams.has("client_version")) {
@@ -1572,7 +1641,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1572
1641
  // The projection is opt-in. Keep the default path free of Cursor install detection,
1573
1642
  // and resolve the bundle table once for the whole list rather than once per row.
1574
1643
  const effortRowsEnabled = config.cursorEffortRows === true;
1575
- const effortRowKnownIds = effortRowsEnabled ? knownEffortRowIds(config) : undefined;
1644
+ // Explicit opt-out skips policy resolution and additional rows.
1645
+ const fastRowsEnabled = config.fastRows !== false;
1646
+ // One inventory serves both grammars; building it twice would double the work on a
1647
+ // hot path for no benefit.
1648
+ const effortRowKnownIds = effortRowsEnabled || fastRowsEnabled
1649
+ ? knownEffortRowIds(config)
1650
+ : undefined;
1576
1651
  const privateInference = effortRowsEnabled
1577
1652
  ? detectCursorInstalls().find(install => install.build === "private-inference")
1578
1653
  : undefined;
@@ -1585,7 +1660,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1585
1660
  knownIds: effortRowKnownIds,
1586
1661
  table: cursorEffortTable,
1587
1662
  supportsReasoning: reasoningEfforts.length > 0,
1588
- });
1663
+ }).flatMap(row => expandFastRow(
1664
+ row,
1665
+ // Only the BASE row earns a fast sibling. An effort row already spent the
1666
+ // grammar, and the parser requires the stripped base to be routable, so
1667
+ // `<base>--<effort>--fast` would publish a row no ingress can resolve.
1668
+ row.id === id && nativeFastEligible(metadataId),
1669
+ config,
1670
+ effortRowKnownIds,
1671
+ ));
1589
1672
  };
1590
1673
  const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1591
1674
  // Same rule as the anthropic branch: with the global fast switch on, a client
@@ -1626,7 +1709,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1626
1709
  knownIds: effortRowKnownIds,
1627
1710
  table: cursorEffortTable,
1628
1711
  supportsReasoning: (m.reasoningEfforts ?? []).length > 0,
1629
- });
1712
+ }).flatMap(expanded => expandFastRow(
1713
+ expanded,
1714
+ expanded.id === row.id && catalogRowFastEligible(m),
1715
+ config,
1716
+ effortRowKnownIds,
1717
+ ));
1630
1718
  }));
1631
1719
  const data = [
1632
1720
  ...visibleNatives.flatMap(id => expandedNativeModelRow(id)),
@@ -1745,7 +1833,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1745
1833
  ...admissionFields(admission),
1746
1834
  };
1747
1835
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1748
- const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
1836
+ const response = await handleSearch(req, config, logCtx, turnAdmissionLease, admission);
1749
1837
  addFinalRequestLog(requestId, start, logCtx, response.status,
1750
1838
  response.status === 499 ? { closeReason: "client_cancel" } : undefined);
1751
1839
  return withCors(response, req, policy);
@@ -1847,7 +1935,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1847
1935
  // pre-translation stream + native passthrough callbacks) — do not re-wrap the
1848
1936
  // translated Anthropic stream here.
1849
1937
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1850
- await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
1938
+ await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy),
1851
1939
  req,
1852
1940
  policy,
1853
1941
  ));
@@ -2048,6 +2136,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
2048
2136
  undefined,
2049
2137
  guiSessionCandidate ?? undefined,
2050
2138
  config.runtimeRole ?? "standalone",
2139
+ isApiAuthRequired(config),
2051
2140
  );
2052
2141
  if (guiFile) return guiFile;
2053
2142
  if (url.pathname === "/" && req.method === "GET") {
@@ -2314,6 +2403,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
2314
2403
  }
2315
2404
  }
2316
2405
  } catch (error) {
2406
+ unregisterQuotaAutoRefresh?.();
2317
2407
  userCostOverlayReconciler?.stop();
2318
2408
  backgroundLifecycle?.releaseAfterFailedStart();
2319
2409
  void nativeMainLifecycle.release();
@@ -2339,7 +2429,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
2339
2429
  ? [() => managementIngressRef.stop(closeActiveConnections)]
2340
2430
  : []),
2341
2431
  async () => {
2342
- userCostOverlayReconciler?.stop();
2432
+ try {
2433
+ userCostOverlayReconciler?.stop();
2434
+ } finally {
2435
+ unregisterQuotaAutoRefresh?.();
2436
+ }
2343
2437
  },
2344
2438
  ],
2345
2439
  async () => {