@bitkyc08/opencodex 2.42.0 → 2.43.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/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-Djowl68T.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 +13 -3
  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/kiro-calibration.ts +181 -0
  26. package/src/adapters/kiro.ts +135 -3
  27. package/src/adapters/openai-responses.ts +216 -26
  28. package/src/adapters/responses-code-mode.ts +59 -0
  29. package/src/adapters/tool-catalog-nudge.ts +1 -1
  30. package/src/adapters/xai-schema-analysis.ts +86 -0
  31. package/src/adapters/xai-tool-schema.ts +2 -87
  32. package/src/adapters/xai-web-search.ts +1 -1
  33. package/src/bridge.ts +22 -10
  34. package/src/chat/inbound.ts +11 -3
  35. package/src/claude/inbound-content-options.ts +60 -0
  36. package/src/claude/inbound-model-options.ts +142 -0
  37. package/src/claude/inbound-records.ts +7 -0
  38. package/src/claude/inbound.ts +10 -202
  39. package/src/claude/model-info.ts +45 -0
  40. package/src/cli/account-auth.ts +21 -6
  41. package/src/cli/capabilities.ts +13 -2
  42. package/src/cli/claude.ts +232 -39
  43. package/src/cli/config-command.ts +9 -1
  44. package/src/cli/dispatch.ts +5 -1
  45. package/src/cli/doctor.ts +10 -0
  46. package/src/cli/effort.ts +372 -0
  47. package/src/cli/export-command.ts +3 -9
  48. package/src/cli/help.ts +1 -0
  49. package/src/cli/index.ts +13 -0
  50. package/src/cli/init.ts +4 -0
  51. package/src/cli/model-selection-guidance.ts +30 -0
  52. package/src/cli/models-runtime.ts +3 -2
  53. package/src/cli/models.ts +8 -3
  54. package/src/cli/opencode.ts +4 -1
  55. package/src/cli/provider-runtime.ts +65 -0
  56. package/src/cli/provider.ts +8 -0
  57. package/src/cli/registry.ts +16 -2
  58. package/src/cli/runtime-api.ts +3 -1
  59. package/src/cli/star-prompt.ts +22 -6
  60. package/src/cli/status-probes.ts +168 -0
  61. package/src/cli/status.ts +5 -168
  62. package/src/clients/config-export/constants.ts +69 -0
  63. package/src/clients/config-export/contracts.ts +154 -0
  64. package/src/clients/config-export/dsh.ts +132 -0
  65. package/src/clients/config-export/fast-models.ts +29 -0
  66. package/src/clients/config-export/mcode.ts +83 -0
  67. package/src/clients/config-export/model-metadata.ts +108 -0
  68. package/src/clients/config-export/omp.ts +104 -0
  69. package/src/clients/config-export/zcode.ts +92 -0
  70. package/src/clients/config-export.ts +18 -710
  71. package/src/codex/account-lifecycle.ts +20 -3
  72. package/src/codex/account-usability.ts +2 -0
  73. package/src/codex/auth-api.ts +170 -23
  74. package/src/codex/auth-context.ts +328 -24
  75. package/src/codex/catalog/effort.ts +30 -4
  76. package/src/codex/catalog/metadata.ts +39 -9
  77. package/src/codex/catalog/native-models.ts +53 -12
  78. package/src/codex/catalog/parsing.ts +119 -5
  79. package/src/codex/catalog/provider-fetch.ts +150 -20
  80. package/src/codex/catalog/reserve.ts +52 -0
  81. package/src/codex/catalog/sync.ts +89 -16
  82. package/src/codex/convergence-types.ts +1 -0
  83. package/src/codex/convergence.ts +2 -0
  84. package/src/codex/data/upstream-models.json +1 -1
  85. package/src/codex/forward-transport-headers.ts +25 -0
  86. package/src/codex/inject.ts +3 -28
  87. package/src/codex/legacy-config-keys.ts +68 -0
  88. package/src/codex/log-guard/inspect-schema.ts +137 -0
  89. package/src/codex/log-guard/inspect.ts +2 -134
  90. package/src/codex/loopback-target.ts +54 -0
  91. package/src/codex/main-account-cache.ts +63 -1
  92. package/src/codex/main-account-hard-lock.ts +52 -0
  93. package/src/codex/main-account.ts +3 -1
  94. package/src/codex/management-convergence.ts +3 -0
  95. package/src/codex/model-entitlements.ts +54 -4
  96. package/src/codex/project-config-warnings.ts +92 -2
  97. package/src/codex/prompt-layers/encoding.ts +80 -0
  98. package/src/codex/prompt-layers/paths.ts +54 -0
  99. package/src/codex/prompt-layers/revision.ts +55 -0
  100. package/src/codex/prompt-layers/toml-edit.ts +163 -0
  101. package/src/codex/prompt-layers/toml-read.ts +181 -0
  102. package/src/codex/prompt-layers.ts +14 -520
  103. package/src/codex/quota-auto-refresh-state.ts +16 -0
  104. package/src/codex/quota-auto-refresh.ts +219 -0
  105. package/src/codex/quota-types.ts +51 -0
  106. package/src/codex/quota.ts +252 -93
  107. package/src/codex/reserve-availability.ts +177 -0
  108. package/src/codex/routing.ts +28 -9
  109. package/src/codex/shim.ts +53 -11
  110. package/src/codex/subagent-model-fallback.ts +23 -3
  111. package/src/combos/failover.ts +125 -7
  112. package/src/combos/identifiers.ts +89 -0
  113. package/src/combos/index.ts +4 -0
  114. package/src/combos/resolve.ts +80 -9
  115. package/src/combos/types.ts +20 -93
  116. package/src/config/subagent-models.ts +24 -0
  117. package/src/config.ts +156 -13
  118. package/src/generated/compatibility-version.json +454 -166
  119. package/src/generated/model-metadata.ts +1 -1
  120. package/src/integrations/journal.ts +65 -4
  121. package/src/integrations/store.ts +5 -0
  122. package/src/lab/events/limits.ts +4 -0
  123. package/src/lib/destination-policy.ts +31 -2
  124. package/src/lib/errors.ts +39 -0
  125. package/src/lib/provider-outbound.ts +69 -3
  126. package/src/lib/proxy-env.ts +22 -0
  127. package/src/lib/redact-folding.ts +176 -0
  128. package/src/lib/redact.ts +2 -175
  129. package/src/lib/state-store-sweeper.ts +20 -6
  130. package/src/lib/token-estimate.ts +94 -27
  131. package/src/lib/windows-user-principal.ts +53 -5
  132. package/src/oauth/anthropic-routing.ts +99 -3
  133. package/src/oauth/generic-account-failover.ts +36 -13
  134. package/src/oauth/index.ts +127 -26
  135. package/src/oauth/login-cli.ts +5 -0
  136. package/src/oauth/meta-muse.ts +117 -15
  137. package/src/oauth/pool-settings-capability.ts +15 -4
  138. package/src/providers/api-keys.ts +8 -10
  139. package/src/providers/default-aliases.ts +39 -0
  140. package/src/providers/derive.ts +10 -2
  141. package/src/providers/fastwire.ts +36 -7
  142. package/src/providers/initial-model-selection-runtime.ts +90 -0
  143. package/src/providers/initial-model-selection.ts +120 -0
  144. package/src/providers/key-failover.ts +134 -54
  145. package/src/providers/key-store.ts +11 -1
  146. package/src/providers/label.ts +1 -1
  147. package/src/providers/model-discovery.ts +76 -0
  148. package/src/providers/model-rename-startup.ts +72 -8
  149. package/src/providers/openai-sidecar.ts +17 -5
  150. package/src/providers/openai-tiers-destination.ts +102 -0
  151. package/src/providers/openai-tiers.ts +2 -99
  152. package/src/providers/opencode-go-transport.ts +41 -0
  153. package/src/providers/quota-key-accounts.ts +141 -0
  154. package/src/providers/quota-types.ts +9 -0
  155. package/src/providers/quota.ts +535 -104
  156. package/src/providers/registry.ts +60 -17
  157. package/src/providers/xai-responses-opt-in.ts +31 -5
  158. package/src/quota/reset-activation.ts +81 -0
  159. package/src/quota/reset-detector.ts +305 -0
  160. package/src/quota/reset-notify-config.ts +162 -0
  161. package/src/quota/reset-observer.ts +125 -0
  162. package/src/quota/reset-poller.ts +160 -0
  163. package/src/quota/reset-seen-store.ts +385 -0
  164. package/src/quota/reset-sinks.ts +199 -0
  165. package/src/quota/window-mapping.ts +106 -0
  166. package/src/responses/apply-patch-envelope.ts +46 -0
  167. package/src/responses/code-mode-helper-compat.ts +39 -1
  168. package/src/responses/custom-tool-compat.ts +10 -4
  169. package/src/responses/hosted-tool-policy.ts +12 -4
  170. package/src/responses/parser-content.ts +133 -0
  171. package/src/responses/parser-text-format.ts +24 -0
  172. package/src/responses/parser-tools.ts +188 -0
  173. package/src/responses/parser.ts +3 -326
  174. package/src/responses/state.ts +124 -28
  175. package/src/router.ts +48 -13
  176. package/src/routing/analytics.ts +1 -0
  177. package/src/routing/capability.ts +17 -4
  178. package/src/server/auth-cors.ts +7 -1
  179. package/src/server/background-lifecycle.ts +23 -1
  180. package/src/server/chat-completions.ts +25 -3
  181. package/src/server/claude-messages.ts +62 -5
  182. package/src/server/effort-row.ts +1 -1
  183. package/src/server/fast-row.ts +295 -0
  184. package/src/server/gui-static.ts +30 -4
  185. package/src/server/index.ts +107 -21
  186. package/src/server/management/agent-settings-routes.ts +2 -2
  187. package/src/server/management/combo-routes.ts +37 -9
  188. package/src/server/management/config-routes.ts +93 -2
  189. package/src/server/management/integration-routes.ts +108 -0
  190. package/src/server/management/model-routes.ts +13 -3
  191. package/src/server/management/model-rows.ts +20 -1
  192. package/src/server/management/native-integration-routes.ts +4 -1
  193. package/src/server/management/oauth-account-routes.ts +38 -10
  194. package/src/server/management/provider-routes.ts +34 -3
  195. package/src/server/management/quota-reset-routes.ts +57 -0
  196. package/src/server/management/route-registry.ts +7 -4
  197. package/src/server/management/shared.ts +19 -5
  198. package/src/server/management/system-routes.ts +3 -2
  199. package/src/server/management-api.ts +14 -2
  200. package/src/server/ports.ts +12 -2
  201. package/src/server/relay-eager.ts +38 -23
  202. package/src/server/relay.ts +4 -0
  203. package/src/server/request-log.ts +6 -0
  204. package/src/server/responses/codex-ws-correlation.ts +65 -0
  205. package/src/server/responses/codex-ws-exchange.ts +261 -0
  206. package/src/server/responses/codex-ws-metadata.ts +134 -0
  207. package/src/server/responses/codex-ws-pool.ts +162 -0
  208. package/src/server/responses/codex-ws-request.ts +87 -0
  209. package/src/server/responses/codex-ws-session.ts +93 -0
  210. package/src/server/responses/codex-ws-wire.ts +144 -0
  211. package/src/server/responses/collaboration.ts +41 -1
  212. package/src/server/responses/compact.ts +105 -12
  213. package/src/server/responses/core.ts +476 -57
  214. package/src/server/responses/fetch-helpers.ts +10 -3
  215. package/src/server/responses/input-admission.ts +16 -9
  216. package/src/server/responses/responses-field-backfill.ts +1 -1
  217. package/src/server/responses/ws-upstream.ts +34 -318
  218. package/src/server/responses-custom-tool-repair.ts +20 -4
  219. package/src/server/responses-undeclared-tool-guard.ts +100 -8
  220. package/src/server/safe-response-headers.ts +23 -0
  221. package/src/server/search.ts +9 -0
  222. package/src/server/subagent-models-startup.ts +27 -0
  223. package/src/server/system-env-shell.ts +238 -0
  224. package/src/server/system-env.ts +7 -234
  225. package/src/server/ws-bridge.ts +3 -25
  226. package/src/server/xai-responses-startup.ts +21 -0
  227. package/src/service-manager-probe.ts +1 -1
  228. package/src/service.ts +55 -16
  229. package/src/types/config.ts +108 -12
  230. package/src/types/provider.ts +36 -7
  231. package/src/types/tools.ts +26 -1
  232. package/src/types.ts +2 -0
  233. package/src/update/notify.ts +8 -2
  234. package/src/usage/cost.ts +38 -28
  235. package/src/usage/expected-prices.ts +34 -15
  236. package/src/usage/log.ts +2 -0
  237. package/src/usage/model-identity.ts +26 -0
  238. package/src/usage/summary.ts +15 -1
  239. package/src/vision/describe.ts +6 -0
  240. package/src/vision/image-rewrite.ts +108 -0
  241. package/src/vision/index.ts +19 -306
  242. package/src/vision/plan.ts +205 -0
  243. package/src/web-search/executor.ts +6 -0
  244. package/src/web-search/index.ts +8 -1
  245. package/gui/dist/assets/index-BU1tE0sr.js +0 -112
  246. package/gui/dist/assets/index-DL9-iS6J.css +0 -1
@@ -28,7 +28,7 @@ import {
28
28
  } from "../../combos";
29
29
  import { isInjectionDebugEnabled } from "../../lib/debug-settings";
30
30
  import { injectionDebugLog } from "../../lib/injection-debug-log";
31
- import { modelInList, namespacedToolName, toolChoiceToolPredicate } from "../../types";
31
+ import { dottedToolName, modelInList, namespacedToolName, toolChoiceToolPredicate } from "../../types";
32
32
  import type { AdapterEvent, OcxConfig, OcxParsedRequest, OcxProviderConfig, OcxProviderContinuationState, OcxUsage } from "../../types";
33
33
  import {
34
34
  forceRefreshOAuthAccessSnapshot,
@@ -122,6 +122,34 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
122
122
  const requestedTools = parsed.context.tools ?? [];
123
123
  const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice, requestedTools);
124
124
  const authorizedTools = requestedTools.filter(toolAllowed);
125
+ // A dotted alias is only safe while it names ONE tool. Namespaces and names both come from
126
+ // the caller's tool catalog and may contain dots, so `{ns: "a", name: "b.c"}` and
127
+ // `{ns: "a.b", name: "c"}` flatten to the same "a.b.c". Registering both would let a dotted
128
+ // provider echo restore against whichever was inserted last, which is a dispatch decision made
129
+ // by declaration order. Resolve ownership across the whole catalog FIRST so the outcome does
130
+ // not depend on that order, then register only the aliases that stayed unambiguous.
131
+ const dottedAliasOwners = new Map<string, string | null>();
132
+ for (const t of authorizedTools) {
133
+ if (!t.namespace) continue;
134
+ const alias = dottedToolName(t.namespace, t.name);
135
+ const identity = JSON.stringify([t.namespace, t.name]);
136
+ const owner = dottedAliasOwners.get(alias);
137
+ if (owner === undefined) dottedAliasOwners.set(alias, identity);
138
+ else if (owner !== identity) dottedAliasOwners.set(alias, null);
139
+ }
140
+ // A dotted alias that shadows a canonical `ns__name` or a bare declaration is the same
141
+ // confusion wearing a different spelling, so those lose the alias too.
142
+ for (const t of authorizedTools) {
143
+ const canonical = namespacedToolName(t.namespace, t.name);
144
+ const owner = dottedAliasOwners.get(canonical);
145
+ if (owner !== undefined && owner !== JSON.stringify([t.namespace, t.name])) {
146
+ dottedAliasOwners.set(canonical, null);
147
+ }
148
+ const bare = dottedAliasOwners.get(t.name);
149
+ if (bare !== undefined && bare !== JSON.stringify([t.namespace, t.name])) {
150
+ dottedAliasOwners.set(t.name, null);
151
+ }
152
+ }
125
153
  for (const t of authorizedTools) {
126
154
  // Upstream output is untrusted: only restore calls for tools the caller authorized.
127
155
  const wireName = namespacedToolName(t.namespace, t.name);
@@ -133,6 +161,18 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
133
161
  if (t.namespace) {
134
162
  budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([wireName, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
135
163
  toolNsMap.set(wireName, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
164
+ // Dotted echo alias (`ns.name`, #3402): same tool identity as the flattened wire name,
165
+ // so a provider that echoes the dotted spelling still restores against this entry.
166
+ const dottedName = dottedToolName(t.namespace, t.name);
167
+ // Ambiguous aliases were resolved to null above; skipping them falls back to the
168
+ // unambiguous `ns__name` form, which every provider can still echo.
169
+ if (dottedAliasOwners.get(dottedName) !== null && dottedName !== wireName) {
170
+ budget?.chargeRetained(new TextEncoder().encode(dottedName).byteLength, { kind: "retained_collectors" });
171
+ declaredToolNames.add(dottedName);
172
+ budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([dottedName, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
173
+ toolNsMap.set(dottedName, { namespace: t.namespace, name: t.name, ...(t.freeform ? { freeform: true } : {}) });
174
+ if (t.parameters && typeof t.parameters === "object") toolParameterSchemas.set(dottedName, t.parameters);
175
+ }
136
176
  }
137
177
  if (t.freeform) {
138
178
  budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
@@ -42,6 +42,8 @@ import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSide
42
42
  import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
43
43
  import {
44
44
  applyCodexAuthContextToProvider,
45
+ createCodexReserveDispatchGuard,
46
+ unwrapUpstreamRetryEvidenceError,
45
47
  CodexMainProfileDrainingError,
46
48
  headersForCodexAuthContext,
47
49
  materializeCodexUpstreamAuthAsync,
@@ -93,7 +95,12 @@ import {
93
95
  import type { DataPlaneAdmission } from "../auth-cors";
94
96
  import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
95
97
  import { CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint } from "../../providers/openai-tiers";
98
+ import { NATIVE_RESERVE_MODEL } from "../../codex/catalog/native-models";
99
+ import { isCodexReserveRequestEligible } from "../../codex/loopback-target";
96
100
  import { slugsEquivalent } from "../../providers/slug-codec";
101
+ import { decideTier, tierValueAfterDecision } from "../../providers/fastwire";
102
+ import { fastPolicyForModel } from "../../providers/service-tier";
103
+ import { parseFastOnlyRowId } from "../fast-row";
97
104
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
98
105
  import { isUsageDebugEnabled } from "../../usage/debug";
99
106
  import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
@@ -219,6 +226,9 @@ export function compactResponseTooLargeError(): Response {
219
226
 
220
227
  async function refreshNativeMainCompactContext(args: {
221
228
  req: Request;
229
+ config: OcxConfig;
230
+ modelId?: string;
231
+ admission?: DataPlaneAdmission;
222
232
  authCtx: CodexAuthContext;
223
233
  provider: OcxProviderConfig;
224
234
  codexAccountMode?: CodexAccountMode;
@@ -228,7 +238,7 @@ async function refreshNativeMainCompactContext(args: {
228
238
  | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
229
239
  | { ok: false; response: Response }
230
240
  > {
231
- const { req, authCtx, provider, codexAccountMode, substituteMainCredential, options } = args;
241
+ const { req, config, authCtx, provider, codexAccountMode, substituteMainCredential, options } = args;
232
242
  if (authCtx.kind !== "main-pool") {
233
243
  return { ok: false, response: formatErrorResponse(401, "authentication_error", "No native main credential to refresh") };
234
244
  }
@@ -252,6 +262,9 @@ async function refreshNativeMainCompactContext(args: {
252
262
  );
253
263
  const headers = new Headers({ "content-type": "application/json" });
254
264
  const selected = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
265
+ admission: args.admission,
266
+ config,
267
+ modelId: args.modelId,
255
268
  substituteMainCredential,
256
269
  signal: req.signal,
257
270
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -270,7 +283,9 @@ async function refreshNativeMainCompactContext(args: {
270
283
  if (req.signal.aborted) {
271
284
  return { ok: false, response: formatErrorResponse(499, "client_cancelled", "Client cancelled compact request") };
272
285
  }
273
- return { ok: false, response: nativeMainRefreshFailureResponse(error) };
286
+ return { ok: false, response: mapCodexAuthContextErrorToResponse(error, {
287
+ now: Date.now(),
288
+ }) ?? nativeMainRefreshFailureResponse(error) };
274
289
  }
275
290
  }
276
291
 
@@ -286,6 +301,9 @@ function isTerminalCompactPoolRefreshFailure(error: unknown): boolean {
286
301
  */
287
302
  async function refreshPoolCompactContext(args: {
288
303
  req: Request;
304
+ config: OcxConfig;
305
+ modelId?: string;
306
+ admission?: DataPlaneAdmission;
289
307
  authCtx: CodexAuthContext & { kind: "pool" };
290
308
  provider: OcxProviderConfig;
291
309
  codexAccountMode?: CodexAccountMode;
@@ -295,7 +313,7 @@ async function refreshPoolCompactContext(args: {
295
313
  | { ok: true; authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers }
296
314
  | { ok: false; response: Response; quarantine: boolean; quarantineGeneration?: number }
297
315
  > {
298
- const { req, authCtx, provider, codexAccountMode, substituteMainCredential, options } = args;
316
+ const { req, config, authCtx, provider, codexAccountMode, substituteMainCredential, options } = args;
299
317
  const reauthResponse = () => formatErrorResponse(
300
318
  401,
301
319
  "authentication_error",
@@ -328,6 +346,9 @@ async function refreshPoolCompactContext(args: {
328
346
  );
329
347
  const headers = new Headers({ "content-type": "application/json" });
330
348
  const selected = await materializeCodexUpstreamAuthAsync(req.headers, refreshedAuthCtx, {
349
+ admission: args.admission,
350
+ config,
351
+ modelId: args.modelId,
331
352
  substituteMainCredential,
332
353
  signal: req.signal,
333
354
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -375,11 +396,13 @@ async function resolveAlternateCompactContext(args: {
375
396
  selectedModelId: string | undefined;
376
397
  excludeAccountId: string | null;
377
398
  turnAdmissionLease?: AdmissionLease;
399
+ admission?: DataPlaneAdmission;
378
400
  }): Promise<{ authCtx: CodexAuthContext; provider: OcxProviderConfig; headers: Headers } | null> {
379
401
  const { req, config, route, selectedModelId, excludeAccountId, turnAdmissionLease } = args;
380
402
  if (!route.codexAccountMode || !excludeAccountId) return null;
381
403
  try {
382
404
  const authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
405
+ admission: args.admission,
383
406
  ...(selectedModelId ? { modelId: selectedModelId } : {}),
384
407
  excludeAccountId,
385
408
  requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
@@ -391,7 +414,7 @@ async function resolveAlternateCompactContext(args: {
391
414
  if (authCtx.accountId === excludeAccountId) return null;
392
415
  const provider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
393
416
  const headers = new Headers({ "content-type": "application/json" });
394
- const selected = headersForCodexAuthContext(req.headers, authCtx);
417
+ const selected = headersForCodexAuthContext(req.headers, authCtx, config, selectedModelId, args.admission);
395
418
  for (const name of FORWARD_HEADERS) {
396
419
  const value = selected.get(name);
397
420
  if (value) headers.set(name, value);
@@ -503,6 +526,16 @@ export async function handleResponsesCompact(
503
526
  if (typeof raw.model !== "string" || raw.model.length === 0) {
504
527
  return formatErrorResponse(400, "invalid_request_error", "compaction request requires a model");
505
528
  }
529
+ // Correct the IDENTITY before routing, or the synthetic id does not route at all. Held in
530
+ // a local rather than written back to `raw.model`: assigning to the property widens it out
531
+ // of the `string` narrowing the guard above just established.
532
+ const compactFastRow = parseFastOnlyRowId(config, () => raw.model as string);
533
+ const compactModel = compactFastRow ? compactFastRow.baseId : raw.model;
534
+ if (compactFastRow) (raw as Record<string, unknown>).model = compactModel;
535
+ // The client's own selector, kept for the request log: `raw.model` is rewritten to the
536
+ // base id above, and logCtx.requestedModel is assigned from it further down, so without
537
+ // this the log would lose which id the client actually asked for.
538
+ const compactRequestedModel = compactFastRow ? compactFastRow.baseId + "--fast" : raw.model;
506
539
 
507
540
  let route;
508
541
  try {
@@ -512,7 +545,7 @@ export async function handleResponsesCompact(
512
545
  // Codex selects a bare native model for compaction even when the operator
513
546
  // routes ordinary turns elsewhere (#2901); the compaction-scoped router
514
547
  // may land that on the configured default provider instead of 404.
515
- route = routeCompactionModel(config, raw.model, evidenceFromBody(raw));
548
+ route = routeCompactionModel(config, compactModel, evidenceFromBody(raw));
516
549
  } catch (err) {
517
550
  if (err instanceof NoEligiblePolicyCandidateError) {
518
551
  // Persist the evaluation trace (per-candidate exclusions + the
@@ -529,7 +562,7 @@ export async function handleResponsesCompact(
529
562
  // exactly the selector form back down the native compact endpoint this guard exists to avoid.
530
563
  // `route.modelId` is the same value `applyCodexAccountGatedWireNormalization` uses in core.ts.
531
564
  const accountGatedCompactWireModel = codexAccountGatedCanonicalWireModel(selectedModelId);
532
- logCtx.requestedModel = raw.model;
565
+ logCtx.requestedModel = compactRequestedModel;
533
566
  logCtx.model = selectedModelId;
534
567
  logCtx.routeDecision = route.routeDecision;
535
568
  logCtx.provider = route.codexAccountNamespace
@@ -544,13 +577,40 @@ export async function handleResponsesCompact(
544
577
  } else {
545
578
  logCtx.resolvedModel = route.modelId;
546
579
  }
580
+ if (compactFastRow) {
581
+ // Resolved AFTER the virtual-model rewrite above, and against `route.modelId`, which is
582
+ // now the WIRE model: `resolveOpenAiCompactModel` maps an alias like `gpt-5.6-sol-pro`
583
+ // onto a different wire id, and capability overrides are keyed by exact model id, so
584
+ // deciding before the rewrite could set `priority` on a wire model that does not support
585
+ // it. `capabilityProvider` is passed for the same reason core.ts:2103 passes it.
586
+ const decision = decideTier(
587
+ fastPolicyForModel(
588
+ route.provider,
589
+ route.modelId,
590
+ route.providerName,
591
+ "responses",
592
+ config.providers[route.providerName],
593
+ ),
594
+ config.fastMode,
595
+ "priority",
596
+ );
597
+ // The WHOLE decision: native compact spreads `raw` into the forwarded body, so on a drop
598
+ // a caller's pre-existing service_tier must be REMOVED rather than left to ride along
599
+ // past the suppression.
600
+ const serviceTier = tierValueAfterDecision(decision, "priority");
601
+ if (serviceTier === undefined) delete (raw as Record<string, unknown>).service_tier;
602
+ else (raw as Record<string, unknown>).service_tier = serviceTier;
603
+ }
547
604
 
548
605
  // #1686: a bearer-presented admission secret is one of ours, so the stored main credential
549
606
  // is substituted below instead of the caller bearer being forwarded.
550
607
  // #2132: and only when the route is a native Codex one, which is the only route that can
551
608
  // consume that credential. See the longer note in core.ts resolveResponsesCodexAuth.
609
+ const customReserveForward = selectedModelId === NATIVE_RESERVE_MODEL
610
+ && isCodexReserveRequestEligible(config, admission)
611
+ && isCanonicalOpenAiForwardProvider(route.provider);
552
612
  const substituteMainCredential = admission?.source === "bearer"
553
- && route.codexAccountMode !== undefined;
613
+ && (route.codexAccountMode !== undefined || customReserveForward);
554
614
  const requestScopedMainCredential = route.codexAccountMode !== undefined
555
615
  && !substituteMainCredential
556
616
  && hasForwardableCodexBearer(req.headers, config);
@@ -598,8 +658,9 @@ export async function handleResponsesCompact(
598
658
  let compactProvider = route.provider;
599
659
  let headers = new Headers({ "content-type": "application/json" });
600
660
  try {
601
- if (route.codexAccountMode) {
602
- authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
661
+ if (route.codexAccountMode || customReserveForward) {
662
+ if (route.codexAccountMode) authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
663
+ admission,
603
664
  accountId: route.codexAccountId,
604
665
  modelId: selectedModelId,
605
666
  substituteMainCredentialForDirect: substituteMainCredential,
@@ -610,6 +671,10 @@ export async function handleResponsesCompact(
610
671
  });
611
672
  logCtx.accountLogLabel = codexAuthContextLogLabel(authCtx, config);
612
673
  const selected = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
674
+ admission,
675
+ config: isCanonicalOpenAiForwardProvider(route.provider) ? config : undefined,
676
+ modelId: selectedModelId,
677
+ beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
613
678
  substituteMainCredential,
614
679
  signal: req.signal,
615
680
  nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
@@ -726,6 +791,7 @@ export async function handleResponsesCompact(
726
791
  sendProvider: OcxProviderConfig,
727
792
  sendHeaders: Headers,
728
793
  recovery: "normal" | "single",
794
+ sendAuthCtx: CodexAuthContext,
729
795
  ): Promise<Response> => {
730
796
  const doFetch = (upstreamRecovery?: UpstreamSendRecovery) => fetchWithHeaderTimeout(
731
797
  compactUrl,
@@ -740,6 +806,8 @@ export async function handleResponsesCompact(
740
806
  providerFetch(sendProvider, undefined, {
741
807
  providerName: route.providerName,
742
808
  modelId: route.modelId,
809
+ beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
810
+ ? createCodexReserveDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
743
811
  }),
744
812
  // Every credential-bearing forward send gets manual redirects, not only
745
813
  // pool sends: direct mode carries the caller's credential too (#914).
@@ -758,17 +826,30 @@ export async function handleResponsesCompact(
758
826
  // The account each outcome belongs to. Reassigned only when the alternate send below
759
827
  // actually happens, so every recorder call names the context that produced it.
760
828
  let outcomeCtx = authCtx;
829
+ const localDispatchRefusal = (error: unknown): Response | undefined => {
830
+ const response = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(error), {
831
+ now: Date.now(), accountSelector: route.codexAccountNamespace,
832
+ });
833
+ if (response) {
834
+ releaseUpstreamHostAdmission(compactHostAdmissionLease);
835
+ compactHostAdmissionLease = null;
836
+ releaseCodexAuthContextProbeLease(outcomeCtx);
837
+ }
838
+ return response;
839
+ };
761
840
  let upstream: Response;
762
841
  let storedPool401ReplayAttempted = false;
763
842
  try {
764
843
  // Same connect timeout + keep-alive reset + transient-5xx recovery as /v1/responses —
765
844
  // compact hits the same ChatGPT host and must soft-avoid / clear affinity (#186).
766
- upstream = await sendCompactAttempt(compactProvider, headers, "normal");
845
+ upstream = await sendCompactAttempt(compactProvider, headers, "normal", authCtx);
767
846
  } catch (err) {
768
847
  if (req.signal.aborted) {
769
848
  recordCompactPoolOutcome(outcomeCtx, 499);
770
849
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
771
850
  }
851
+ const localRefusal = localDispatchRefusal(err);
852
+ if (localRefusal) return localRefusal;
772
853
  const outcome = classifyTransportFailureKind(err);
773
854
  // Host-level evidence stands regardless of pool membership (#914 review).
774
855
  if (outcome === "connect_neutral") {
@@ -801,6 +882,9 @@ export async function handleResponsesCompact(
801
882
  const poolReplay = poolAuthCtx
802
883
  ? await refreshPoolCompactContext({
803
884
  req,
885
+ admission,
886
+ config,
887
+ modelId: selectedModelId,
804
888
  authCtx: poolAuthCtx,
805
889
  provider: compactProvider,
806
890
  codexAccountMode: route.codexAccountMode,
@@ -811,6 +895,9 @@ export async function handleResponsesCompact(
811
895
  const replay = poolReplay
812
896
  ?? await refreshNativeMainCompactContext({
813
897
  req,
898
+ admission,
899
+ config,
900
+ modelId: selectedModelId,
814
901
  authCtx,
815
902
  provider: compactProvider,
816
903
  codexAccountMode: route.codexAccountMode,
@@ -841,12 +928,14 @@ export async function handleResponsesCompact(
841
928
  headers = replay.headers;
842
929
  logCtx.accountLogLabel = codexAuthContextLogLabel(replay.authCtx, config);
843
930
  try {
844
- upstream = await sendCompactAttempt(compactProvider, headers, "single");
931
+ upstream = await sendCompactAttempt(compactProvider, headers, "single", authCtx);
845
932
  } catch (err) {
846
933
  if (req.signal.aborted) {
847
934
  recordCompactPoolOutcome(outcomeCtx, 499);
848
935
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
849
936
  }
937
+ const localRefusal = localDispatchRefusal(err);
938
+ if (localRefusal) return localRefusal;
850
939
  recordCompactPoolOutcome(outcomeCtx, classifyTransportFailureKind(err));
851
940
  return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
852
941
  }
@@ -874,6 +963,7 @@ export async function handleResponsesCompact(
874
963
  // throws, the first rejection is still intact and can be returned to the client.
875
964
  const alternate = await resolveAlternateCompactContext({
876
965
  req,
966
+ admission,
877
967
  config,
878
968
  route,
879
969
  selectedModelId,
@@ -900,6 +990,7 @@ export async function handleResponsesCompact(
900
990
  authCtx.accountId,
901
991
  upstream.headers,
902
992
  authCtx.writerGeneration,
993
+ authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
903
994
  );
904
995
  }
905
996
  recordCompactPoolOutcome(authCtx, upstream.status, {
@@ -911,12 +1002,14 @@ export async function handleResponsesCompact(
911
1002
  outcomeCtx = alternate.authCtx;
912
1003
  logCtx.accountLogLabel = codexAuthContextLogLabel(alternate.authCtx, config);
913
1004
  try {
914
- upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single");
1005
+ upstream = await sendCompactAttempt(alternate.provider, alternate.headers, "single", alternate.authCtx);
915
1006
  } catch (err) {
916
1007
  if (req.signal.aborted) {
917
1008
  recordCompactPoolOutcome(outcomeCtx, 499);
918
1009
  return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
919
1010
  }
1011
+ const localRefusal = localDispatchRefusal(err);
1012
+ if (localRefusal) return localRefusal;
920
1013
  const outcome = classifyTransportFailureKind(err);
921
1014
  // Host-level evidence stands regardless of pool membership (#914 review).
922
1015
  if (outcome === "connect_neutral") {