@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -1,29 +1,32 @@
1
1
  import { timingSafeEqual } from "node:crypto";
2
2
  import { formatErrorResponse } from "../bridge";
3
+ import {
4
+ codexAutoStartEnabled,
5
+ modelPreferHostedToolsConfigError,
6
+ providerModelCostsConfigError,
7
+ requestPacingConfigError,
8
+ retryOn429PolicyConfigError,
9
+ sanitizeModelCostsForDisplay,
10
+ } from "../config";
3
11
  import {
4
12
  apiKeyTransportConfigError,
5
13
  booleanRecordConfigError,
6
14
  modelAdapterRecordConfigError,
7
- modelPreferHostedToolsConfigError,
8
- codexAutoStartEnabled,
9
15
  nonBlankStringArrayConfigError,
10
16
  positiveIntegerConfigError,
11
17
  positiveIntegerRecordConfigError,
12
18
  providerBaseUrlConfigError,
13
19
  providerHeadersConfigError,
14
- providerModelCostsConfigError,
15
20
  reasoningSummaryDeliveryRecordConfigError,
16
- retryOn429PolicyConfigError,
17
- requestPacingConfigError,
18
- sanitizeModelCostsForDisplay,
19
21
  upstreamHttpVersionConfigError,
20
- } from "../config";
22
+ } from "../config/provider-validation";
21
23
  import { providerDestinationConfigError } from "../lib/destination-policy";
22
24
  import { redactSecretString } from "../lib/redact";
23
25
  import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
24
26
  import { providerConfigSeed } from "../providers/derive";
25
27
  import type { OcxConfig, OcxProviderConfig } from "../types";
26
28
  import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
29
+ import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compact-budget";
27
30
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
28
31
  import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
29
32
 
@@ -563,6 +566,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
563
566
  if (contextOverlayError) return contextOverlayError;
564
567
  delete canonicalCandidate.contextWindow;
565
568
  delete canonicalCandidate.modelContextWindows;
569
+ // User-owned soft compaction policy; it does not alter the canonical transport seed.
570
+ delete canonicalCandidate.modelAutoCompactTokenLimits;
566
571
  const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
567
572
  if (!canonical) {
568
573
  return `provider ${name} must equal the canonical built-in provider seed`;
@@ -605,6 +610,13 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
605
610
  if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
606
611
  const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
607
612
  if (maxInputError) return `provider ${name} ${maxInputError}`;
613
+ const autoCompactError = modelAutoCompactTokenLimitsConfigError(
614
+ raw.modelAutoCompactTokenLimits,
615
+ { requireNativeIds: name === "openai" },
616
+ );
617
+ if (autoCompactError) {
618
+ return `provider ${JSON.stringify(redactSecretString(name))} ${autoCompactError}`;
619
+ }
608
620
  const reasoningSummariesError = booleanRecordConfigError(raw.modelSupportsReasoningSummaries, "modelSupportsReasoningSummaries");
609
621
  if (reasoningSummariesError) return `provider ${name} ${reasoningSummariesError}`;
610
622
  const reasoningSummaryDeliveryError = reasoningSummaryDeliveryRecordConfigError(
@@ -694,6 +706,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
694
706
  }
695
707
  for (const key of [
696
708
  "defaultModel",
709
+ "alias",
710
+ "modelAliases",
711
+ "defaultAliases",
697
712
  "disabled",
698
713
  "allowPrivateNetwork",
699
714
  "authMode",
@@ -705,6 +720,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
705
720
  "models",
706
721
  "contextWindow",
707
722
  "modelContextWindows",
723
+ "modelAutoCompactTokenLimits",
708
724
  "defaultMaxOutputTokens",
709
725
  "modelMaxOutputTokens",
710
726
  "openRouterRouting",
@@ -744,8 +760,12 @@ export function safeConfigDTO(config: OcxConfig): unknown {
744
760
  port: config.port,
745
761
  hostname: config.hostname ?? "127.0.0.1",
746
762
  defaultProvider: config.defaultProvider,
763
+ defaultModelAliases: config.defaultModelAliases,
747
764
  codexAutoStart: codexAutoStartEnabled(config),
748
765
  websockets: config.websockets,
766
+ // The GUI's browser-open toggle reads and writes this; absent means the
767
+ // historical auto-open behavior.
768
+ oauthOpenBrowser: config.oauthOpenBrowser !== false,
749
769
  providers,
750
770
  };
751
771
  }
@@ -18,7 +18,7 @@ import {
18
18
  responsesJsonToChatCompletion,
19
19
  responsesSseToChatCompletionsSse,
20
20
  } from "../chat/outbound";
21
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
21
+ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
22
22
  import { redactSecretString } from "../lib/redact";
23
23
  import { resolveClientRetryAfter } from "../lib/retry-after";
24
24
  import { estimateTokens } from "../lib/token-estimate";
@@ -120,6 +120,7 @@ async function handleChatCompletionsWithBudget(
120
120
  logCtx.model = route.modelId;
121
121
  logCtx.providerAdapter = route.provider.adapter;
122
122
  logCtx.requestedModel = requestedModel;
123
+ if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel;
123
124
  logCtx.provider = route.providerName;
124
125
  logCtx.routeDecision = route.routeDecision;
125
126
  settledRoute = route;
@@ -280,26 +281,26 @@ async function handleChatCompletionsWithBudget(
280
281
  const parsed = JSON.parse(text) as {
281
282
  error?: { message?: string; type?: string; code?: string | null } | string;
282
283
  message?: string;
284
+ type?: string;
285
+ code?: string | null;
283
286
  };
284
287
  const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined;
285
288
  const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
286
289
  const rawFallback = text
287
290
  ? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`
288
291
  : message;
289
- message = nested?.message || flat || rawFallback;
290
- if (nested) {
291
- if (typeof nested.type === "string") upstreamType = nested.type;
292
- if (nested.code === null || typeof nested.code === "string") upstreamCode = nested.code;
293
- }
292
+ const upstreamMessage = nested?.message || flat;
293
+ message = upstreamMessage
294
+ ? redactSecretString(upstreamMessage).slice(0, 500)
295
+ : rawFallback;
296
+ const structuredType = nested?.type ?? parsed.type;
297
+ const structuredCode = nested?.code ?? parsed.code;
298
+ if (typeof structuredType === "string") upstreamType = structuredType;
299
+ if (structuredCode === null || typeof structuredCode === "string") upstreamCode = structuredCode;
294
300
  } catch {
295
301
  if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
296
302
  }
297
303
  } catch { /* keep fallback */ }
298
- const retryAfter = resolveClientRetryAfter({
299
- status: upstream.status,
300
- message,
301
- upstreamRetryAfter: upstream.headers.get("retry-after"),
302
- });
303
304
  const classified = classifyError(
304
305
  upstream.status,
305
306
  upstreamType
@@ -309,9 +310,9 @@ async function handleChatCompletionsWithBudget(
309
310
  : "invalid_request_error"),
310
311
  message,
311
312
  );
312
- if (isCyberPolicyCode(upstreamCode)) {
313
+ if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
313
314
  classified.code = CYBER_POLICY_ERROR_CODE;
314
- classified.type = "invalid_request_error";
315
+ classified.type = cyberPolicyErrorType(upstreamType);
315
316
  } else if (upstreamCode === "model_not_found") {
316
317
  // Structured model_not_found must win over classifyError's generic remaps.
317
318
  classified.code = "model_not_found";
@@ -320,6 +321,13 @@ async function handleChatCompletionsWithBudget(
320
321
  classified.code = upstreamCode;
321
322
  }
322
323
  const status = isCyberPolicyCode(classified.code) ? 400 : upstream.status;
324
+ const retryAfter = isCyberPolicyCode(classified.code)
325
+ ? undefined
326
+ : resolveClientRetryAfter({
327
+ status: upstream.status,
328
+ message,
329
+ upstreamRetryAfter: upstream.headers.get("retry-after"),
330
+ });
323
331
  const rewritten = new Response(JSON.stringify({
324
332
  error: {
325
333
  message: classified.message,
@@ -386,14 +394,14 @@ async function handleChatCompletionsWithBudget(
386
394
  const status = (json as Rec)?.status;
387
395
  if (status === "failed") {
388
396
  const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error;
389
- const message = error?.message ?? "upstream request failed";
397
+ const message = redactSecretString(error?.message ?? "upstream request failed");
390
398
  const classified = classifyError(502, error?.type ?? "server_error", message);
391
399
  if (error?.code === "translation_buffer_limit") {
392
400
  classified.code = "translation_buffer_limit";
393
401
  classified.type = "upstream_error";
394
- } else if (isCyberPolicyCode(error?.code)) {
402
+ } else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
395
403
  classified.code = CYBER_POLICY_ERROR_CODE;
396
- classified.type = "invalid_request_error";
404
+ classified.type = cyberPolicyErrorType(error?.type);
397
405
  } else if (error?.code === "model_not_found") {
398
406
  // Same deliberate preserve as the non-OK path: structured code beats generic classify.
399
407
  classified.code = "model_not_found";
@@ -1,5 +1,5 @@
1
1
  import { chatCompletionsErrorBody } from "../chat/outbound";
2
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
2
+ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
3
3
  import { redactSecretString } from "../lib/redact";
4
4
  import {
5
5
  isTranslatorBudgetExceededError,
@@ -226,9 +226,9 @@ export function nativeChatSse(
226
226
  if (error) {
227
227
  const status = error.status ?? 502;
228
228
  const classified = classifyError(status, error.type ?? "upstream_error", error.message);
229
- if (isCyberPolicyCode(error.code)) {
229
+ if (isCyberPolicyCode(error.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
230
230
  classified.code = CYBER_POLICY_ERROR_CODE;
231
- classified.type = "invalid_request_error";
231
+ classified.type = cyberPolicyErrorType(error.type);
232
232
  } else if (error.code !== undefined && error.code !== null) {
233
233
  classified.code = error.code;
234
234
  }
@@ -6,7 +6,13 @@ import {
6
6
  collectChatCompletion,
7
7
  isChatCompletionsStreamError,
8
8
  } from "../chat/outbound";
9
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
9
+ import {
10
+ classifyError,
11
+ cyberPolicyErrorType,
12
+ CYBER_POLICY_ERROR_CODE,
13
+ isCyberPolicyCode,
14
+ isCyberPolicyMessage,
15
+ } from "../lib/errors";
10
16
  import type { AdmissionLease } from "../lib/admission";
11
17
  import { readBoundedResponseBody } from "../lib/bounded-body";
12
18
  import { redactSecretString } from "../lib/redact";
@@ -281,13 +287,24 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
281
287
  const detail = activeAdapter.formatErrorBody?.(response.status, response.headers, bodyText) ?? "";
282
288
  let upstreamType: string | undefined;
283
289
  let upstreamCode: string | null | undefined;
290
+ let upstreamMessage: string | undefined;
284
291
  try {
285
292
  const parsedError = JSON.parse(bodyText) as Rec;
286
293
  const nested = isRec(parsedError.error) ? parsedError.error : undefined;
287
- if (typeof nested?.type === "string") upstreamType = nested.type;
288
- if (nested?.code === null || typeof nested?.code === "string") upstreamCode = nested.code;
294
+ const details = nested ?? parsedError;
295
+ if (typeof details.type === "string") upstreamType = details.type;
296
+ if (details.code === null || typeof details.code === "string") upstreamCode = details.code;
297
+ const rawMessage = typeof details.message === "string"
298
+ ? details.message
299
+ : typeof parsedError.error === "string" ? parsedError.error : undefined;
300
+ if (rawMessage?.trim()) {
301
+ upstreamMessage = redactSecretString(rawMessage.trim());
302
+ }
289
303
  } catch { /* keep generic classification */ }
290
- const message = detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
304
+ const message = upstreamMessage
305
+ && (isCyberPolicyCode(upstreamCode) || isCyberPolicyMessage(upstreamMessage))
306
+ ? upstreamMessage
307
+ : detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`;
291
308
  const classified = classifyError(
292
309
  response.status,
293
310
  upstreamType ?? (response.status === 401 ? "authentication_error"
@@ -295,9 +312,9 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
295
312
  : response.status >= 500 ? "server_error" : "invalid_request_error"),
296
313
  message,
297
314
  );
298
- if (isCyberPolicyCode(upstreamCode)) {
315
+ if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
299
316
  classified.code = CYBER_POLICY_ERROR_CODE;
300
- classified.type = "invalid_request_error";
317
+ classified.type = cyberPolicyErrorType(upstreamType);
301
318
  } else if (upstreamCode === "model_not_found") {
302
319
  classified.code = "model_not_found";
303
320
  classified.type = "invalid_request_error";
@@ -305,11 +322,13 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
305
322
  classified.code = upstreamCode;
306
323
  }
307
324
  const status = isCyberPolicyCode(classified.code) ? 400 : response.status;
308
- const retryAfter = resolveClientRetryAfter({
309
- status: response.status,
310
- message: classified.message,
311
- upstreamRetryAfter: response.headers.get("retry-after"),
312
- });
325
+ const retryAfter = isCyberPolicyCode(classified.code)
326
+ ? undefined
327
+ : resolveClientRetryAfter({
328
+ status: response.status,
329
+ message: classified.message,
330
+ upstreamRetryAfter: response.headers.get("retry-after"),
331
+ });
313
332
  finishLog(status, classified.message);
314
333
  return new Response(JSON.stringify(chatCompletionsErrorBody(status, classified.message, classified.type, classified.code)), {
315
334
  status,
@@ -553,7 +553,7 @@ export async function fetchWithHeaderDeadline(
553
553
  ): Promise<HeaderDeadlineFetchResult> {
554
554
  const deadline = makeDeadline(timeoutMs, parent);
555
555
  try {
556
- const upstream = await fetchImpl(input, { ...init, signal: deadline.signal });
556
+ const upstream = await fetchImpl(input, { ...init, signal: deadline.signal, timeout: 0 });
557
557
  return { kind: "response", upstream };
558
558
  } catch (error) {
559
559
  if (deadline.didExpire()) return { kind: "timeout" };
@@ -1,4 +1,5 @@
1
1
  import { markActivity } from "../lib/sidecar-tracker";
2
+ import { knownModelIdsForProvider } from "../router";
2
3
  import {
3
4
  buildWarmupCompletionFrames,
4
5
  buildWsErrorFrame,
@@ -22,10 +23,11 @@ import {
22
23
  import { reconcileOAuthProviders } from "../oauth";
23
24
  import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
24
25
  import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
25
- import { getCodexHome } from "../codex/paths";
26
+ import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../service";
26
27
  import { shouldSyncCodexOnStart } from "../codex/desired-state";
27
28
  import {
28
29
  inspectNativeCodexOwnership,
30
+ type NativeCodexOwnership,
29
31
  type OwnershipInspection,
30
32
  } from "../integrations/native/ownership-preflight";
31
33
  import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
@@ -108,6 +110,7 @@ import {
108
110
  type RequestLogContext,
109
111
  type RequestLogEntry,
110
112
  } from "./request-log";
113
+ import { sessionLaneIdFromRequest } from "./request-log-conversation";
111
114
  export {
112
115
  addFinalRequestLog,
113
116
  filterRequestLogs,
@@ -174,8 +177,8 @@ import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
174
177
  import {
175
178
  bindNativeMainStartupLifecycle,
176
179
  blockNativeMainStartupForUnownedServiceHome,
180
+ prepareNativeMainStartupLifecycle,
177
181
  releaseNativeMainStartupLifecycle,
178
- startNativeMainStartupLifecycle,
179
182
  type NativeMainStartupGateDeps,
180
183
  type NativeMainStartupLifecycle,
181
184
  } from "../codex/native-profile-startup";
@@ -199,6 +202,11 @@ import {
199
202
  import { SYSTEM_RESTART_CAPABILITY_VERSION } from "../lib/system-restart-contract";
200
203
  import { LOCAL_PROVIDER_RELOAD_CAPABILITY_VERSION } from "../lib/local-provider-reload-contract";
201
204
  import { createReadinessGate, type ReadinessGate } from "./readiness";
205
+ import {
206
+ createRuntimePackageTreeIntegrityGuard,
207
+ type PackageTreeIntegrityGuard,
208
+ } from "../lib/package-tree-integrity";
209
+ import { detectInstall } from "../update/index";
202
210
 
203
211
  export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
204
212
  const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
@@ -443,17 +451,34 @@ export interface StartServerDeps {
443
451
  nativeMainStartup?: NativeMainStartupGateDeps;
444
452
  /** Test-only ownership evidence; production inspects the installed service state. */
445
453
  inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
454
+ /** Test-only service-home resolver; production resolves the current homes directly. */
455
+ resolveServiceHomes?: typeof currentServiceHomes;
446
456
  /** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
447
457
  liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
448
458
  /** Test-only seam; production derives a fresh local-attestation secret per process. */
449
459
  localAttestationSecret?: string;
450
460
  /** Optional readiness gate; a fresh pending gate is created when omitted. */
451
461
  readinessGate?: ReadinessGate;
462
+ /** Test-only package-tree observation; production captures package.json identity at boot. */
463
+ packageTreeIntegrity?: PackageTreeIntegrityGuard;
452
464
  }
453
465
 
454
- function inspectStartupOwnership(deps: StartServerDeps): OwnershipInspection {
466
+ function inspectStartupOwnership(
467
+ deps: StartServerDeps,
468
+ currentHomes: ReturnType<typeof currentServiceHomes> | null,
469
+ statePaths: readonly string[] | null,
470
+ ): OwnershipInspection {
455
471
  try {
456
- return (deps.inspectNativeCodexOwnership ?? inspectNativeCodexOwnership)();
472
+ if (currentHomes === null || statePaths === null) {
473
+ return {
474
+ ownership: "unknown",
475
+ reason: "startup service-home resolution failed",
476
+ };
477
+ }
478
+ if (deps.inspectNativeCodexOwnership) {
479
+ return deps.inspectNativeCodexOwnership({ currentHomes, statePaths });
480
+ }
481
+ return inspectNativeCodexOwnership({ currentHomes, statePaths });
457
482
  } catch {
458
483
  return {
459
484
  ownership: "unknown",
@@ -540,15 +565,27 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
540
565
  // journal, or credential path. Both positive foreign evidence and an unprovable
541
566
  // ownership state are non-authority.
542
567
  startupCacheInvalidationWrote = false;
543
- const startupCacheOwnership = inspectStartupOwnership(deps);
568
+ const resolveServiceHomes = deps.resolveServiceHomes ?? currentServiceHomes;
569
+ let startupOwnershipHomes: ReturnType<typeof currentServiceHomes> | null = null;
570
+ let startupOwnershipStatePaths: readonly string[] | null = null;
571
+ try {
572
+ const homes = resolveServiceHomes();
573
+ const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
574
+ startupOwnershipHomes = homes;
575
+ startupOwnershipStatePaths = statePaths;
576
+ } catch { /* inspection below stays unknown */ }
577
+ const startupCacheOwnership = inspectStartupOwnership(
578
+ deps,
579
+ startupOwnershipHomes,
580
+ startupOwnershipStatePaths,
581
+ );
544
582
  // Startup cache invalidation is best-effort and must never block the server from
545
- // serving. It now takes K so it cannot race a convergence commit, but both the
546
- // home resolution and the acquisition can fail on a machine with no Codex home —
547
- // `getCodexHome()` THROWS when CODEX_HOME names a missing directory, which would
548
- // otherwise turn "no Codex installed" into "proxy will not start".
549
- if (startupCacheOwnership.ownership === "owned") {
583
+ // serving. It now takes K so it cannot race a convergence commit. Use the home
584
+ // paired with the ownership inspection; re-reading ambient CODEX_HOME here could
585
+ // invalidate a different installation after an environment or mount change.
586
+ if (startupCacheOwnership.ownership === "owned" && startupOwnershipHomes !== null) {
550
587
  try {
551
- const startupCodexHome = getCodexHome();
588
+ const startupCodexHome = startupOwnershipHomes.codexHome;
552
589
  // #1046: record whether this actually rewrote the cache. `handleStart` ORs this
553
590
  // with the later startup sync and warns ONCE about stale app-servers; warning
554
591
  // here instead would read a catalog mtime the sync is about to move.
@@ -668,12 +705,21 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
668
705
  }), req, policy);
669
706
  }
670
707
 
708
+ function packageTreeChangedResponse(req: Request, policy: RequestPolicyView, message: string): Response {
709
+ return withCors(new Response(JSON.stringify({
710
+ error: { type: "server_error", code: "package_tree_changed", message },
711
+ }), {
712
+ status: 503,
713
+ headers: { "Content-Type": "application/json" },
714
+ }), req, policy);
715
+ }
716
+
671
717
  async function runAdmittedHttpTurn(
672
718
  req: Request,
673
719
  policy: RequestPolicyView,
674
720
  work: (lease: ActiveTurnLease) => Promise<Response>,
675
721
  ): Promise<Response> {
676
- const lease = tryAdmitTurn();
722
+ const lease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
677
723
  if (!lease) return serverBusyResponse(req, "active turns", policy);
678
724
  let response: Response;
679
725
  try {
@@ -694,6 +740,8 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
694
740
  // passes it in, and transitions it after the post-startup sync settles. When
695
741
  // no gate is supplied (tests, ad-hoc starts) a fresh pending gate is created.
696
742
  const readinessGate = deps.readinessGate ?? createReadinessGate();
743
+ const packageTreeIntegrity = deps.packageTreeIntegrity
744
+ ?? createRuntimePackageTreeIntegrityGuard(detectInstall());
697
745
  // Actual bound port, filled in after Bun.serve binds so /readyz reports the
698
746
  // real ephemeral port for startServer(0). /healthz keeps its existing port
699
747
  // field (the requested listenPort) byte-for-byte.
@@ -705,18 +753,71 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
705
753
  // clients; no Codex request can use this lifecycle in that state.
706
754
  // Re-probe here instead of trusting the earlier cache decision: startup work
707
755
  // between the two sites must not widen the service-install race.
708
- const nativeOwnership = inspectStartupOwnership(deps);
756
+ const nativeOwnership = inspectStartupOwnership(deps, startupOwnershipHomes, startupOwnershipStatePaths);
757
+ const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
758
+ && startupOwnershipHomes !== null
759
+ ? prepareNativeMainStartupLifecycle(
760
+ deps.nativeMainStartup,
761
+ { codexHome: startupOwnershipHomes.codexHome, configDir: startupOwnershipHomes.opencodexHome },
762
+ )
763
+ : null;
764
+ let retryOwnershipHomes = startupOwnershipHomes;
765
+ let retryOwnershipStatePaths = startupOwnershipStatePaths;
766
+ let retryPreparedNativeMainLifecycle = preparedNativeMainLifecycle;
767
+ const reprobeNativeOwnership = (): NativeCodexOwnership => {
768
+ // If startup could not resolve the homes at all, preserve a bounded retry
769
+ // without guessing an authority. The first successful resolution is pinned
770
+ // together with its service-state paths before ownership is inspected.
771
+ if (retryOwnershipHomes === null || retryOwnershipStatePaths === null) {
772
+ try {
773
+ const homes = resolveServiceHomes();
774
+ const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
775
+ retryOwnershipHomes = homes;
776
+ retryOwnershipStatePaths = statePaths;
777
+ } catch {
778
+ return "unknown";
779
+ }
780
+ }
781
+ const homes = retryOwnershipHomes;
782
+ const statePaths = retryOwnershipStatePaths;
783
+ const answer = inspectStartupOwnership(deps, homes, statePaths).ownership;
784
+ if (answer !== "owned") return answer;
785
+ retryPreparedNativeMainLifecycle ??= prepareNativeMainStartupLifecycle(
786
+ deps.nativeMainStartup,
787
+ { codexHome: homes.codexHome, configDir: homes.opencodexHome },
788
+ );
789
+ // An ownership verdict without a lifecycle bound to that same home is not
790
+ // enough to reopen native-main admission.
791
+ return retryPreparedNativeMainLifecycle ? "owned" : "unknown";
792
+ };
793
+ const ownershipRetryOptions = {
794
+ reprobe: reprobeNativeOwnership,
795
+ expectedHomeId: () => retryPreparedNativeMainLifecycle?.homeId ?? null,
796
+ startOwnedLifecycle: () => {
797
+ if (!retryPreparedNativeMainLifecycle) {
798
+ throw new Error("Native-main ownership became known before its startup lifecycle was prepared.");
799
+ }
800
+ return retryPreparedNativeMainLifecycle.start();
801
+ },
802
+ };
709
803
  const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
710
804
  ? nativeOwnership.ownership === "owned"
711
- ? startNativeMainStartupLifecycle(deps.nativeMainStartup)
712
- : blockNativeMainStartupForUnownedServiceHome(
713
- nativeOwnership.ownership === "foreign" ? "foreign-ownership" : "ownership-unknown",
714
- // #2108: an `unknown` verdict means the probe could not answer, not that this host
715
- // is unownable. Hand the fence a way to re-ask so a host that becomes answerable
716
- // after boot reopens on its own instead of needing `ocx restart`. A `foreign`
717
- // verdict ignores this by design — that one is a fact, not a question.
718
- { reprobe: () => inspectStartupOwnership(deps).ownership },
719
- )
805
+ ? preparedNativeMainLifecycle
806
+ ? preparedNativeMainLifecycle.start()
807
+ : blockNativeMainStartupForUnownedServiceHome(
808
+ "ownership-unknown",
809
+ ownershipRetryOptions,
810
+ )
811
+ : nativeOwnership.ownership === "foreign"
812
+ ? blockNativeMainStartupForUnownedServiceHome("foreign-ownership")
813
+ : blockNativeMainStartupForUnownedServiceHome(
814
+ "ownership-unknown",
815
+ // #2108: an `unknown` verdict means the probe could not answer, not that this host
816
+ // is unownable. Hand the fence a way to re-ask so a host that becomes answerable
817
+ // after boot reopens on its own instead of needing `ocx restart`. A `foreign`
818
+ // verdict ignores this by design — that one is a fact, not a question.
819
+ ownershipRetryOptions,
820
+ )
720
821
  : {
721
822
  homeId: null,
722
823
  settled: Promise.resolve({ status: "ready", homeId: null }),
@@ -767,6 +868,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
767
868
  if (decoded === "/readyz" || decoded === "/readyz/") readyzPath = decoded;
768
869
  } catch { /* malformed encoding — not a readiness path */ }
769
870
 
871
+ const packageTreeStatus = packageTreeIntegrity.status();
872
+ if (!packageTreeStatus.ok && (
873
+ url.pathname === "/healthz"
874
+ || readyzPath !== undefined
875
+ || url.pathname.startsWith("/v1/")
876
+ )) {
877
+ const message = "OpenCodex package files changed while this proxy was running; restart OpenCodex before retrying.";
878
+ const response = url.pathname === "/healthz" || readyzPath !== undefined
879
+ ? jsonResponse({
880
+ status: "restart_required",
881
+ service: "opencodex",
882
+ version: VERSION,
883
+ uptime: process.uptime(),
884
+ pid: process.pid,
885
+ port: boundPort ?? requestServer.port ?? listenPort,
886
+ error: { code: "package_tree_changed", message },
887
+ }, 503, req, policy)
888
+ : packageTreeChangedResponse(req, policy, message);
889
+ const headers = new Headers(response.headers);
890
+ headers.set("Retry-After", "5");
891
+ return new Response(response.body, { status: 503, headers });
892
+ }
893
+
770
894
  if (req.method === "OPTIONS") {
771
895
  // /readyz is exact-GET only; OPTIONS (like POST and the trailing-slash
772
896
  // path) must answer the deterministic JSON 404, never the generic 204
@@ -815,7 +939,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
815
939
  // unauthenticated loopback listener (#1102) is a second Bun.serve, and handing its
816
940
  // request to the public server's upgrade would fail or cross sockets.
817
941
  if (requestServer.upgrade(req, {
818
- data: buildResponsesWsData(selectForwardHeaders(req.headers), admission, websocketLease),
942
+ data: buildResponsesWsData(
943
+ selectForwardHeaders(req.headers),
944
+ admission,
945
+ websocketLease,
946
+ sessionLaneIdFromRequest(req.headers),
947
+ ),
819
948
  })) return undefined as unknown as Response;
820
949
  websocketLease.release();
821
950
  return withCors(formatErrorResponse(426, "upgrade_required", "WebSocket upgrade failed"), req, policy);
@@ -946,6 +1075,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
946
1075
  : [];
947
1076
  const disabledNatives = disabledNativeSlugs(config);
948
1077
  const disabledModels = new Set(config.disabledModels ?? []);
1078
+ const exactComboSlugs = exactComboCatalogSlugs(config);
949
1079
  const shadowedNativeSlugs = configuredNativeAliasSlugs(config);
950
1080
  const suppressedBareNativeSlugs = new Set([
951
1081
  ...desktopAllowlistSuppressedNativeSlugs(config),
@@ -1031,7 +1161,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1031
1161
  config.subagentModels,
1032
1162
  websocketsEnabled(config),
1033
1163
  maMode as "v1" | "default" | "v2",
1034
- exactComboCatalogSlugs(config),
1164
+ exactComboSlugs,
1035
1165
  accountSelectors,
1036
1166
  suppressedBareNativeSlugs,
1037
1167
  new Set(),
@@ -1108,12 +1238,29 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1108
1238
  const data = [
1109
1239
  ...visibleNatives.map(id => nativeModelRow(id)),
1110
1240
  ...visibleAccountNatives.map(({ id, metadataId }) => nativeModelRow(id, metadataId)),
1111
- ...uniqueCatalogModelsForRawPublicList(goOrdered).map(m => ({
1112
- id: m.alias ?? `${m.provider}/${m.id}`,
1113
- object: "model",
1114
- created: 0,
1115
- owned_by: m.owned_by ?? m.provider,
1116
- ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1241
+ ...await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
1242
+ const publicId = m.alias ?? `${m.provider}/${m.id}`;
1243
+ const isCombo = m.provider === "combo" && exactComboSlugs.has(publicId);
1244
+ const provider = config.providers[m.provider];
1245
+ const effective = provider
1246
+ ? (await import("../providers/default-aliases")).effectiveModelAliases(
1247
+ config,
1248
+ provider,
1249
+ knownModelIdsForProvider(m.provider, provider, config),
1250
+ ).get(m.id)
1251
+ : undefined;
1252
+ return {
1253
+ id: publicId,
1254
+ object: "model",
1255
+ created: 0,
1256
+ // This endpoint is an OpenAI-compatible inbound contract. Some clients use
1257
+ // owned_by as an adapter selector, so a virtual combo must name that wire
1258
+ // adapter rather than the internal catalog authority marker.
1259
+ owned_by: isCombo ? "openai" : (m.owned_by ?? m.provider),
1260
+ ...(isCombo ? { is_combo: true } : {}),
1261
+ ...(effective ? { alias_of: `${provider?.alias || m.provider}/${effective.alias}` } : {}),
1262
+ ...grokEffortFields(m.reasoningEfforts ?? [], m.defaultReasoningEffort),
1263
+ };
1117
1264
  })),
1118
1265
  ];
1119
1266
  return jsonResponse({ object: "list", data }, 200, req, policy);
@@ -1420,7 +1567,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1420
1567
  provider: "unknown",
1421
1568
  ...admissionFields(admission),
1422
1569
  };
1423
- const turnAdmissionLease = tryAdmitTurn();
1570
+ const turnAdmissionLease = tryAdmitTurn(sessionLaneIdFromRequest(req.headers));
1424
1571
  if (!turnAdmissionLease) return serverBusyResponse(req, "active turns", policy);
1425
1572
  let resolved;
1426
1573
  try {
@@ -1572,7 +1719,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1572
1719
  return;
1573
1720
  }
1574
1721
 
1575
- const turnAdmissionLease = tryAdmitTurn();
1722
+ const turnAdmissionLease = tryAdmitTurn(ws.data.sessionLaneId);
1576
1723
  if (!turnAdmissionLease) {
1577
1724
  sendJsonFrame(ws, buildWsErrorFrame(503, {
1578
1725
  type: "server_error",