@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
package/src/router.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  } from "./combos";
10
10
  import type { NormalizedComboConfig } from "./combos/types";
11
11
  import { hasOwnProvider } from "./config/provider-name";
12
- import { resolveProviderApiKey } from "./providers/key-store";
12
+ import { providerUsesKeyAuthOverride, resolveProviderApiKey } from "./providers/key-store";
13
13
  import { assertProviderDestinationAllowed } from "./lib/destination-policy";
14
14
  import { redactSecretString, redactUrlForLog } from "./lib/redact";
15
15
  import {
@@ -42,10 +42,17 @@ import {
42
42
  type RouteDecisionTraceV1,
43
43
  type TraceCandidateInput,
44
44
  } from "./routing/trace";
45
- import { getRoutingProfile, resolvePolicyProfileId } from "./routing/profile";
45
+ import { getRoutingProfile, resolvePolicyProfileId, POLICY_NAMESPACE } from "./routing/profile";
46
46
  import { evaluatePolicyProfile, type PolicyRequestEvidence } from "./routing/evaluator";
47
47
  import { assemblePolicyCandidateEvidence } from "./routing/compatibility/assemble";
48
48
 
49
+ export class UnknownRoutingPolicyError extends Error {
50
+ constructor(readonly profileId: string) {
51
+ super(`Unknown routing policy: ${profileId}`);
52
+ this.name = "UnknownRoutingPolicyError";
53
+ }
54
+ }
55
+
49
56
  export class NoEligiblePolicyCandidateError extends Error {
50
57
  /** Evaluation trace (with per-candidate exclusions) when nothing qualified. */
51
58
  readonly trace?: RouteDecisionTraceV1;
@@ -300,10 +307,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
300
307
  const repairLegacyMimoFreeAuth = providerName === "mimo-free"
301
308
  && staticModelCatalog
302
309
  && (provider.authMode === undefined || provider.authMode === "local");
303
- const explicitKeyOverride = registryEntry.authKind === "oauth"
304
- && registryEntry.allowKeyAuthOverride === true
305
- && provider.authMode === "key"
306
- && resolvedApiKey !== undefined;
310
+ const explicitKeyOverride = providerUsesKeyAuthOverride(registryEntry, provider, resolvedApiKey);
307
311
  const canonicalAuthMode = explicitKeyOverride
308
312
  ? "key"
309
313
  : repairLegacyMimoFreeAuth
@@ -604,11 +608,13 @@ function routeModelInternal(
604
608
  // configured profile alias executes the policy evaluator and routes the
605
609
  // selected candidate. Only explicit requests reach this branch; concrete
606
610
  // recursive targets skip policy resolution entirely (bypassCombos) so an
607
- // alias matching a selected candidate can never recurse, and a
608
- // `policy/<id>` without a configured profile falls through to normal
609
- // provider/default resolution instead of failing.
611
+ // alias matching a selected candidate can never recurse. Missing reserved
612
+ // policy selectors fail before ordinary provider/default resolution.
610
613
  const policyId = !bypassCombos ? resolvePolicyProfileId(config, modelId) : null;
611
614
  const profile = policyId ? getRoutingProfile(config, policyId) : undefined;
615
+ if (!bypassCombos && !profile && (policyId !== null || modelId.startsWith(`${POLICY_NAMESPACE}/`))) {
616
+ throw new UnknownRoutingPolicyError(policyId ?? modelId.slice(POLICY_NAMESPACE.length + 1));
617
+ }
612
618
  if (profile && policyId) {
613
619
  // One clock read per decision keeps candidate evidence, exclusions, and
614
620
  // scores mutually consistent and reproducible.
@@ -679,10 +685,39 @@ function routeModelInternal(
679
685
  // no such provider exists.
680
686
  if (slash > 0) {
681
687
  const requestedProvider = modelId.slice(0, slash);
682
- const provName = hasOwnProvider(config.providers, requestedProvider)
683
- ? requestedProvider
684
- : Object.entries(config.providers).find(([, provider]) =>
685
- typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0];
688
+ const requestedLower = requestedProvider.toLowerCase();
689
+ let provName: string | undefined;
690
+
691
+ if (hasOwnProvider(config.providers, requestedProvider)) {
692
+ provName = requestedProvider;
693
+ } else {
694
+ // Pass 1: explicit configured provider aliases (operator override always wins)
695
+ const configuredMatches = Object.entries(config.providers).filter(([, provider]) =>
696
+ typeof provider.alias === "string" && provider.alias.trim().toLowerCase() === requestedLower,
697
+ );
698
+ if (configuredMatches.length === 1) {
699
+ provName = configuredMatches[0]![0];
700
+ } else if (configuredMatches.length > 1) {
701
+ throw new Error("provider alias '" + requestedProvider + "' is ambiguous: " + configuredMatches.map(([n]) => n).sort().join(", "));
702
+ } else {
703
+ // Pass 2: built-in registry aliases, only for providers that do NOT have an explicit alias override
704
+ // and whose registry alias has not been claimed by another configured provider (#3531 review)
705
+ const registryMatches = Object.entries(config.providers).filter(([name, provider]) => {
706
+ if (provider.alias !== undefined) return false;
707
+ const regAlias = PROVIDER_REGISTRY.find(e => e.id === name)?.alias;
708
+ if (!regAlias || regAlias.toLowerCase() !== requestedLower) return false;
709
+ const claimedByOther = Object.entries(config.providers).some(([otherName, p]) =>
710
+ otherName !== name && typeof p.alias === "string" && p.alias.trim().toLowerCase() === requestedLower
711
+ );
712
+ return !claimedByOther;
713
+ });
714
+ if (registryMatches.length === 1) {
715
+ provName = registryMatches[0]![0];
716
+ } else if (registryMatches.length > 1) {
717
+ throw new Error("provider alias '" + requestedProvider + "' is ambiguous across registry fallbacks: " + registryMatches.map(([n]) => n).sort().join(", "));
718
+ }
719
+ }
720
+ }
686
721
  if (!provName) {
687
722
  // A genuine slash-containing native model id still falls through unchanged.
688
723
  } else {
@@ -116,6 +116,7 @@ interface Bucket extends AnalyticsBreakdownRow {
116
116
 
117
117
  const COOLDOWN_RECOVERY_KINDS = new Set([
118
118
  "rate-limit-429",
119
+ "key-401",
119
120
  "key-429",
120
121
  "oauth-401",
121
122
  "anthropic-oauth-429",
@@ -213,9 +213,18 @@ export function candidateCapabilityEvidence(
213
213
  || provider?.parallelToolCalls === true
214
214
  || undefined;
215
215
 
216
- const reasoningEfforts = modelRecordValue(provider?.modelReasoningEfforts, modelId)
217
- ?? modelRecordValue(registryEntry?.modelReasoningEfforts, modelId)
218
- ?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
216
+ // `noReasoningModels` is a POSITIVE statement that this model has no effort control, and
217
+ // every other consumer already reads it that way: configuredReasoningEfforts
218
+ // (reasoning-effort.ts), supportedLadderFor (server/effort-policy.ts) and the
219
+ // compatibility fingerprint (routing/compatibility/behavior.ts) all check it first.
220
+ // Routing evidence did not, which was harmless only while no registry ladder existed to
221
+ // contradict it — a provider-level ladder would otherwise report supported rungs for a
222
+ // model the operator explicitly disabled reasoning for.
223
+ const reasoningEfforts = modelInList(provider?.noReasoningModels, modelId)
224
+ ? []
225
+ : modelRecordValue(provider?.modelReasoningEfforts, modelId)
226
+ ?? modelRecordValue(registryEntry?.modelReasoningEfforts, modelId)
227
+ ?? (isNative ? nativeReasoningEfforts(modelId) : undefined);
219
228
 
220
229
  const tierSupport = provider
221
230
  ? serviceTierSupportForModel(provider, modelId, providerName)
@@ -236,7 +245,11 @@ export function candidateCapabilityEvidence(
236
245
  ...(typeof contextWindow === "number" ? { contextWindow } : {}),
237
246
  ...(typeof image === "boolean" ? { image } : {}),
238
247
  ...(typeof tools === "boolean" ? { tools } : {}),
239
- ...(reasoningEfforts !== undefined && reasoningEfforts.length > 0 ? { reasoningEfforts } : {}),
248
+ // A DEFINED but empty ladder is known-negative evidence and must survive. Dropping it
249
+ // made the evaluator take its `!Array.isArray` branch and record "unknown", which is
250
+ // permissive — "we could not tell" rather than "this model has no effort control" — so
251
+ // an explicitly disabled model could still satisfy a reasoning-effort requirement.
252
+ ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
240
253
  ...(serviceTier !== "unknown" ? { serviceTier } : {}),
241
254
  ...localRemote,
242
255
  ...(typeof encryptedCodexTasks === "boolean" ? { encryptedCodexTasks } : {}),
@@ -1,4 +1,5 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
+ import { initialModelSelection } from "../providers/initial-model-selection";
2
3
  import { extractAccountId } from "../oauth/chatgpt";
3
4
  import { formatErrorResponse } from "../bridge";
4
5
  import {
@@ -62,7 +63,7 @@ export function isLoopbackRequestHost(value: string | null): boolean {
62
63
  // Scope of that guarantee: it holds for Hosts `parseHttpHost` can parse. An unparseable
63
64
  // Host still returns true above — pre-existing behavior, not browser-reachable (a browser
64
65
  // composes Host from its own connection), and pinned by a characterization test in
65
- // tests/server-loopback-host-gate.test.ts. Tightening it is separate work.
66
+ // tests/server/server-loopback-host-gate.test.ts. Tightening it is separate work.
66
67
  return isLoopbackHostname(parsed.hostname);
67
68
  }
68
69
 
@@ -784,6 +785,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
784
785
  modelSupportsServiceTier: "editor",
785
786
  preserveResponsesReasoningContent: "editor",
786
787
  decodesNativeCompactionBlobs: "editor",
788
+ allowEncryptedV2AgentTasks: "editor",
787
789
  allowPrivateNetwork: "editor",
788
790
  upstreamHttpVersion: "editor",
789
791
  upstreamWebsocket: "editor",
@@ -797,6 +799,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
797
799
  models: "editor",
798
800
  liveModels: "editor",
799
801
  selectedModels: "editor",
802
+ initialModelSelection: "runtime",
800
803
  retainModels: "editor",
801
804
  newModelPolicy: "editor",
802
805
  modelPreset: "editor",
@@ -830,6 +833,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
830
833
  modelPreferHostedTools: "editor",
831
834
  supportsOpenAiWebSearchToolFields: "editor",
832
835
  xaiResponsesXSearch: "editor",
836
+ xaiResponsesDefaultVersion: "runtime",
833
837
  supportsResponsesCustomTools: "editor",
834
838
  responsesSnapshotRepair: "editor",
835
839
  reasoningEffortMap: "editor",
@@ -1001,6 +1005,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
1001
1005
  if (name === "xai") {
1002
1006
  dto.xaiResponsesOptInState = xaiResponsesOptInState(provider);
1003
1007
  }
1008
+ const selection = initialModelSelection(provider);
1009
+ if (selection) dto.initialModelSelection = selection;
1004
1010
  providers[name] = dto;
1005
1011
  }
1006
1012
  return {
@@ -10,6 +10,7 @@ import {
10
10
  startStorageCleanupScheduler,
11
11
  stopStorageCleanupScheduler,
12
12
  } from "../storage/policy-scheduler";
13
+ import { startQuotaResetPoller, stopQuotaResetPoller } from "../quota/reset-poller";
13
14
  import {
14
15
  cancelQueuedStorageWorkerSpawns,
15
16
  drainStorageWorkers,
@@ -59,11 +60,31 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
59
60
  stateStoreSweeper = startStateStoreSweeper();
60
61
  setLivePolicyOwner(applyPolicy);
61
62
  startStorageCleanupScheduler();
63
+ // Opt-in: the tick itself is a no-op unless config.quotaResetNotify is enabled with a
64
+ // sink, and the interval is unref'd, so a default install pays one dormant timer.
65
+ startQuotaResetPoller();
66
+ // The configured cadence is resolved out of band: reading it here would put a static edge
67
+ // to the config barrel on the load-time path the quota boundary guard pins.
68
+ void import("../quota/reset-poller")
69
+ .then(poller => poller.syncQuotaResetPollerCadence())
70
+ .catch(() => {
71
+ // The next tick adopts it.
72
+ });
73
+ // Install the delivery sink now rather than waiting out the first poll interval, which is 15
74
+ // minutes by default. Without this, an enabled install would observe nothing for its first
75
+ // quarter hour — including the live request path, which is gated on the sink existing.
76
+ // Fire-and-forget: startup must not await an optional subsystem.
77
+ void import("../quota/reset-activation")
78
+ .then(activation => activation.syncQuotaResetActivation())
79
+ .catch(() => {
80
+ // The next poll tick retries.
81
+ });
62
82
  return { memoryWatchdog, stateStoreSweeper };
63
83
  } catch (error) {
64
84
  memoryWatchdog?.stop();
65
85
  stateStoreSweeper?.stop();
66
86
  stopStorageCleanupScheduler();
87
+ stopQuotaResetPoller();
67
88
  setLivePolicyOwner(null);
68
89
  throw error;
69
90
  }
@@ -75,6 +96,7 @@ function stopProcessLoops(): void {
75
96
  loops?.memoryWatchdog.stop();
76
97
  loops?.stateStoreSweeper.stop();
77
98
  stopStorageCleanupScheduler();
99
+ stopQuotaResetPoller();
78
100
  setLivePolicyOwner(null);
79
101
  }
80
102
 
@@ -179,4 +201,4 @@ export function acquireServerBackgroundLifecycle(
179
201
  releaseFlight = Promise.resolve();
180
202
  },
181
203
  };
182
- }
204
+ }
@@ -22,7 +22,7 @@ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPo
22
22
  import { redactSecretString } from "../lib/redact";
23
23
  import { resolveClientRetryAfter } from "../lib/retry-after";
24
24
  import { estimateTokens } from "../lib/token-estimate";
25
- import { NoEligiblePolicyCandidateError, routeModel } from "../router";
25
+ import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
26
26
  import { evidenceFromBody } from "../routing/request-evidence";
27
27
  import { resolveWireProtocolOverride } from "./adapter-resolve";
28
28
  import type { OcxConfig } from "../types";
@@ -47,6 +47,9 @@ import {
47
47
  } from "../lib/translator-budget";
48
48
  import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
49
49
  import { parseRequestEffortRowId } from "./effort-row";
50
+ import { parseSyntheticRowId } from "./fast-row";
51
+ import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
52
+ import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../codex/loopback-target";
50
53
 
51
54
  type Rec = Record<string, unknown>;
52
55
 
@@ -105,8 +108,15 @@ async function handleChatCompletionsWithBudget(
105
108
  }
106
109
 
107
110
  const requestedModel = chatBody.model as string;
108
- const effortRow = parseRequestEffortRowId(requestedModel, config);
111
+ const { fastRow, effortRow } = parseSyntheticRowId(requestedModel, config);
109
112
  if (effortRow) chatBody.model = effortRow.baseId;
113
+ if (fastRow) {
114
+ chatBody.model = fastRow.baseId;
115
+ // A caller intent; decideTier rules on it downstream. Unlike an effort row this does NOT
116
+ // block the native-chat shortcut below: native chat carries service_tier itself and runs
117
+ // the same policy, so blocking it would degrade the request for no reason.
118
+ chatBody.service_tier = "priority";
119
+ }
110
120
  const stream = chatBody.stream === true;
111
121
  // Best-effort Grok attribution: the managed fence stamps this header on every model
112
122
  // it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage
@@ -138,6 +148,11 @@ async function handleChatCompletionsWithBudget(
138
148
  }
139
149
  if (!effortRow && isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route;
140
150
  } catch (err) {
151
+ if (err instanceof UnknownRoutingPolicyError) {
152
+ logCtx.requestedModel = requestedModel;
153
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
154
+ return chatCompletionsErrorResponse(404, err.message, "invalid_request_error");
155
+ }
141
156
  if (err instanceof NoEligiblePolicyCandidateError) {
142
157
  logCtx.routeDecision = err.trace;
143
158
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
@@ -205,6 +220,13 @@ async function handleChatCompletionsWithBudget(
205
220
  else internalBody.reasoning = next;
206
221
  }
207
222
 
223
+ const visionDescribeTerminal = req.headers.get("x-opencodex-vision-describe") === "1";
224
+ // Concrete helper targets must fail before optional stored-main credential enrichment.
225
+ // Unresolved combos are checked after their concrete child route is selected in Responses.
226
+ if (settledRoute && !settledRoute.combo && isCanonicalOpenAiForwardProvider(settledRoute.provider)
227
+ && isCodexReserveHelperUnsupported(config, settledRoute.modelId, logIds?.admission, visionDescribeTerminal)) {
228
+ return chatCompletionsErrorResponse(400, CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, "invalid_request_error");
229
+ }
208
230
  const headers = new Headers({ "content-type": "application/json" });
209
231
  for (const name of FORWARD_HEADERS) {
210
232
  if (name === "authorization" && !directRoute) continue;
@@ -271,7 +293,7 @@ async function handleChatCompletionsWithBudget(
271
293
  // Terminal vision-describe marker (roadmap 180): the bridge rebuilds
272
294
  // headers from the FORWARD_HEADERS allowlist, which would drop the raw
273
295
  // header — so the fact is detected here and carried as an option flag.
274
- ...(req.headers.get("x-opencodex-vision-describe") === "1" ? { visionDescribeTerminal: true } : {}),
296
+ ...(visionDescribeTerminal ? { visionDescribeTerminal: true } : {}),
275
297
  translatorBudget,
276
298
  ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}),
277
299
  onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }),
@@ -26,7 +26,7 @@ import {
26
26
  } from "../claude/outbound";
27
27
  import { clearableDeadline, idleDeadline } from "../lib/abort";
28
28
  import { estimateTokens } from "../lib/token-estimate";
29
- import { NoEligiblePolicyCandidateError, routeModel } from "../router";
29
+ import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router";
30
30
  import { evidenceFromBody } from "../routing/request-evidence";
31
31
  import { resolveWireProtocolOverride } from "./adapter-resolve";
32
32
  import type { OcxConfig } from "../types";
@@ -40,6 +40,7 @@ import {
40
40
  isDataPlaneAdmissionSecret,
41
41
  isProxyAdmissionSecret,
42
42
  type RequestPolicyView,
43
+ type DataPlaneAdmission,
43
44
  } from "./auth-cors";
44
45
  import type { AdmissionLease } from "../lib/admission";
45
46
  import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission";
@@ -54,9 +55,30 @@ import {
54
55
  parseRequestEffortRowId,
55
56
  type ParsedEffortRowId,
56
57
  } from "./effort-row";
58
+ import {
59
+ parseFastOnlyRowId,
60
+ parseSyntheticRowId,
61
+ type ParsedFastRowId,
62
+ } from "./fast-row";
57
63
 
58
64
  type Rec = Record<string, unknown>;
59
65
 
66
+ /**
67
+ * Decode a Claude selector that may carry the fast marker.
68
+ *
69
+ * The exact form is tried first, so a real model whose alias genuinely ends in the marker
70
+ * keeps winning. Only then is the marker treated as synthetic and the bare base decoded:
71
+ * a Desktop 3P alias is a HASH registered WITHOUT the marker, so an exact lookup can never
72
+ * resolve a synthetic one.
73
+ */
74
+ function decodeClaudeFastSelector(raw: string, cc?: OcxConfig["claudeCode"]): string {
75
+ const exact = resolveInboundModel(raw, cc);
76
+ if (exact !== raw || !raw.endsWith("--fast")) return exact;
77
+ const bare = raw.slice(0, -"--fast".length);
78
+ const decodedBase = resolveInboundModel(bare, cc);
79
+ return decodedBase === bare ? exact : `${decodedBase}--fast`;
80
+ }
81
+
60
82
  function isRec(v: unknown): v is Rec {
61
83
  return !!v && typeof v === "object" && !Array.isArray(v);
62
84
  }
@@ -571,7 +593,7 @@ export async function handleClaudeMessages(
571
593
  req: Request,
572
594
  config: OcxConfig,
573
595
  logCtx: RequestLogContext,
574
- logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
596
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
575
597
  requestPolicy: RequestPolicyView = config,
576
598
  ): Promise<Response> {
577
599
  const translatorBudget = createTranslatorBudget();
@@ -591,7 +613,7 @@ async function handleClaudeMessagesWithBudget(
591
613
  config: OcxConfig,
592
614
  logCtx: RequestLogContext,
593
615
  translatorBudget: TranslatorBudget,
594
- logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease },
616
+ logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission },
595
617
  requestPolicy: RequestPolicyView = config,
596
618
  ): Promise<Response> {
597
619
  logCtx.surface = "claude";
@@ -606,6 +628,7 @@ async function handleClaudeMessagesWithBudget(
606
628
  let cacheKeySource: ClaudeCacheKeySource = null;
607
629
  let effortOverride: string | null = null;
608
630
  let effortRow: ParsedEffortRowId | null = null;
631
+ let fastRow: ParsedFastRowId | null = null;
609
632
  let requestedModel = "";
610
633
  try {
611
634
  anthropicBody = await readAnthropicBody(req, translatorBudget);
@@ -628,11 +651,20 @@ async function handleClaudeMessagesWithBudget(
628
651
  }
629
652
  if (isRec(anthropicBody) && typeof anthropicBody.model === "string") {
630
653
  requestedModel = anthropicBody.model;
631
- effortRow = parseRequestEffortRowId(requestedModel, config);
654
+ // Decode for Fast only. A Claude alias is `claude-ocx-<provider>--<model>`, so it
655
+ // already uses `--` as its own separator: stripping the marker off the RAW alias would
656
+ // turn `claude-ocx-p--foo--fast` into `claude-ocx-p--foo` and route a DIFFERENT model.
657
+ // Effort parsing keeps the raw selector, so its behaviour is untouched.
658
+ ({ fastRow, effortRow } = parseSyntheticRowId(
659
+ requestedModel,
660
+ config,
661
+ () => decodeClaudeFastSelector(requestedModel, config.claudeCode),
662
+ ));
632
663
  if (effortRow) {
633
664
  anthropicBody.model = effortRow.baseId;
634
665
  effortOverride = effortRow.effort;
635
666
  }
667
+ if (fastRow) anthropicBody.model = fastRow.baseId;
636
668
  }
637
669
  // Debug capture (opt-in allowlist scalars) BEFORE the passthrough branch so
638
670
  // native, routed, and disabled-alias paths are all observable (devlog 130 B1).
@@ -657,7 +689,10 @@ async function handleClaudeMessagesWithBudget(
657
689
  );
658
690
  if (claudeConversationId) logCtx.conversationId = claudeConversationId;
659
691
  }
660
- if (!effortRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
692
+ // A fast row blocks passthrough, unlike the chat case: this path forwards to Anthropic's
693
+ // own API, whose wire has no service_tier field and whose FastWire kind has an empty
694
+ // adapter set by design, so the tier would be silently dropped.
695
+ if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
661
696
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
662
697
  }
663
698
  if (isRec(anthropicBody) && effortOverride) {
@@ -669,6 +704,10 @@ async function handleClaudeMessagesWithBudget(
669
704
  }
670
705
  const translation = anthropicToResponsesTranslation(anthropicBody, config.claudeCode);
671
706
  internalBody = translation.body;
707
+ // The Anthropic translator builds its body from model/input/store/stream plus sampling
708
+ // fields only, so the caller intent is applied to the TRANSLATED body rather than the
709
+ // inbound one.
710
+ if (fastRow) internalBody.service_tier = "priority";
672
711
  translatorBudget.chargeRetained(new TextEncoder().encode(JSON.stringify(internalBody)).byteLength, { kind: "request_copies" });
673
712
  cacheKeySource = translation.cacheKeySource;
674
713
  } catch (err) {
@@ -725,6 +764,11 @@ async function handleClaudeMessagesWithBudget(
725
764
  if (ladder !== undefined && ladder.length === 0) delete internalBody.reasoning;
726
765
  }
727
766
  } catch (err) {
767
+ if (err instanceof UnknownRoutingPolicyError) {
768
+ logCtx.requestedModel = requestedModel;
769
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
770
+ return anthropicErrorResponse(404, err.message, "invalid_request_error");
771
+ }
728
772
  if (err instanceof NoEligiblePolicyCandidateError) {
729
773
  logCtx.routeDecision = err.trace;
730
774
  if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" });
@@ -784,6 +828,9 @@ async function handleClaudeMessagesWithBudget(
784
828
  addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
785
829
  };
786
830
  const upstream = await handleResponses(internalReq, buildClaudeReplayConfig(config), logCtx, {
831
+ // Routing keeps Claude-only sidecar overrides; admission policy must follow the live owner.
832
+ codexAuthPolicy: config,
833
+ ...(logIds?.admission ? { admission: logIds.admission } : {}),
787
834
  ...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}),
788
835
  abortSignal: req.signal,
789
836
  promptCacheKeyIsSharedCohort: cacheKeySource === "system",
@@ -1032,6 +1079,16 @@ export async function handleClaudeCountTokens(
1032
1079
  model = stripOneMillionMarker(countRoute);
1033
1080
  raw.model = model;
1034
1081
  }
1082
+ // Fast-only: count_tokens never parsed an effort row, so it must not start. It returns a
1083
+ // token estimate and sends no tier, so only the IDENTITY is corrected - without this the
1084
+ // synthetic id reaches native passthrough as a model Anthropic has never heard of.
1085
+ const countFastRow = parseFastOnlyRowId(
1086
+ config, () => decodeClaudeFastSelector(model, config.claudeCode),
1087
+ );
1088
+ if (countFastRow) {
1089
+ model = countFastRow.baseId;
1090
+ raw.model = model;
1091
+ }
1035
1092
  captureClaudeInbound("count_tokens", raw, resolveInboundModel(model, config.claudeCode), req.headers.get("anthropic-beta") ?? undefined);
1036
1093
  if (wantsNativePassthrough(req, config, requestPolicy, model)) {
1037
1094
  return await anthropicNativePassthrough(req, config, { model, provider: "anthropic-native", surface: "claude" }, undefined, raw, "/v1/messages/count_tokens");
@@ -29,7 +29,7 @@ export interface EffortRowOptions {
29
29
  supportsReasoning?: boolean;
30
30
  }
31
31
 
32
- function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
32
+ export function isKnownId(knownIds: EffortRowKnownIds | undefined, id: string): boolean {
33
33
  return typeof knownIds === "function" ? knownIds(id) : knownIds?.has(id) === true;
34
34
  }
35
35