@bitkyc08/opencodex 2.58.0 → 2.59.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 (188) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +1 -1
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/cursor/catalog.ts +11 -0
  10. package/src/adapters/cursor/effort-map.ts +16 -2
  11. package/src/adapters/cursor/envelope-echo.ts +55 -2
  12. package/src/adapters/cursor/message-mapper.ts +3 -2
  13. package/src/adapters/cursor/protobuf-request.ts +8 -5
  14. package/src/adapters/cursor/request-builder.ts +14 -3
  15. package/src/adapters/cursor/thread-continuity.ts +105 -31
  16. package/src/adapters/cursor/tool-guidance.ts +5 -4
  17. package/src/adapters/cursor.ts +42 -1
  18. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  19. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  20. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  21. package/src/adapters/devin.ts +75 -13
  22. package/src/adapters/google-antigravity-wire.ts +29 -2
  23. package/src/adapters/google-http.ts +8 -1
  24. package/src/adapters/google.ts +23 -4
  25. package/src/adapters/openai-chat/response-events.ts +61 -0
  26. package/src/adapters/openai-chat.ts +5 -10
  27. package/src/adapters/openai-responses/passthrough.ts +10 -1
  28. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  29. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  30. package/src/adapters/responses-tool-schema.ts +76 -46
  31. package/src/adapters/run-turn-queue.ts +17 -4
  32. package/src/bridge/response-json.ts +1 -1
  33. package/src/bridge/sse.ts +165 -24
  34. package/src/claude/context-windows.ts +22 -0
  35. package/src/claude/outbound.ts +35 -4
  36. package/src/cli/account-api.ts +4 -3
  37. package/src/cli/account-extended.ts +22 -2
  38. package/src/cli/account-orca-import.ts +63 -0
  39. package/src/cli/account.ts +32 -4
  40. package/src/cli/capabilities.ts +40 -0
  41. package/src/cli/claude.ts +29 -1
  42. package/src/cli/codex-cli-update.ts +97 -2
  43. package/src/cli/dispatch.ts +54 -0
  44. package/src/cli/doctor.ts +197 -2
  45. package/src/cli/help.ts +4 -1
  46. package/src/cli/index.ts +88 -20
  47. package/src/cli/models-runtime.ts +33 -4
  48. package/src/cli/registry.ts +11 -1
  49. package/src/cli/runtime-api.ts +44 -0
  50. package/src/cli/start-args.ts +94 -0
  51. package/src/cli/system-command.ts +2 -0
  52. package/src/client/machine-api.ts +4 -3
  53. package/src/client/machine-listener.ts +14 -1
  54. package/src/clients/config-export/constants.ts +2 -3
  55. package/src/clients/config-export.ts +5 -5
  56. package/src/codex/account-store.ts +81 -5
  57. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  58. package/src/codex/auth-api/routes.ts +17 -2
  59. package/src/codex/auth-context.ts +16 -12
  60. package/src/codex/catalog/build-entries.ts +25 -4
  61. package/src/codex/catalog/derive-entry.ts +8 -1
  62. package/src/codex/catalog/effort.ts +10 -6
  63. package/src/codex/catalog/gather-capture.ts +1 -0
  64. package/src/codex/catalog/model-hints.ts +37 -5
  65. package/src/codex/catalog/parsing.ts +83 -5
  66. package/src/codex/catalog/reserve-warn.ts +96 -0
  67. package/src/codex/catalog/retained-sync.ts +19 -0
  68. package/src/codex/catalog/routed-gather.ts +42 -3
  69. package/src/codex/cli-installation-identity.ts +210 -0
  70. package/src/codex/cli-installation-targets.ts +158 -0
  71. package/src/codex/convergence.ts +5 -0
  72. package/src/codex/history-provider.ts +4 -1
  73. package/src/codex/history-state-open.ts +105 -0
  74. package/src/codex/inject/config-toml.ts +44 -2
  75. package/src/codex/inject.ts +3 -2
  76. package/src/codex/lineage.ts +83 -32
  77. package/src/codex/loopback-target.ts +31 -0
  78. package/src/codex/main-account-hard-lock.ts +2 -1
  79. package/src/codex/main-account.ts +10 -3
  80. package/src/codex/main-device-reauth.ts +17 -9
  81. package/src/codex/model-entitlements.ts +60 -1
  82. package/src/codex/observed-model-denials.ts +137 -0
  83. package/src/codex/orca-auth-source.ts +94 -0
  84. package/src/codex/orca-import.ts +219 -0
  85. package/src/codex/prompt-text-probe.ts +282 -12
  86. package/src/codex/quota-401-recovery.ts +12 -0
  87. package/src/codex/quota-types.ts +65 -0
  88. package/src/codex/quota.ts +24 -19
  89. package/src/codex/routing/cooldown-math.ts +8 -47
  90. package/src/codex/routing/pin-drain.ts +57 -0
  91. package/src/codex/routing.ts +13 -15
  92. package/src/codex/subagent-model-fallback.ts +94 -0
  93. package/src/codex/windows-installation-files.ts +224 -0
  94. package/src/combos/failover.ts +122 -5
  95. package/src/config/diagnostics.ts +21 -0
  96. package/src/config/load-degrade.ts +15 -0
  97. package/src/config/pending-teardown.ts +8 -0
  98. package/src/config/process-state.ts +36 -3
  99. package/src/config/provider-relative-send-path.ts +16 -0
  100. package/src/config/proxy-env.ts +23 -5
  101. package/src/config/schema/config-schema.ts +21 -0
  102. package/src/config/schema/leaf-validators.ts +64 -17
  103. package/src/generated/compatibility-version.json +235 -163
  104. package/src/generated/model-metadata.ts +1 -1
  105. package/src/lib/bounded-body.ts +4 -2
  106. package/src/lib/destination-policy.ts +48 -6
  107. package/src/lib/errors.ts +3 -15
  108. package/src/lib/local-destinations.ts +32 -5
  109. package/src/lib/provider-outbound.ts +3 -3
  110. package/src/lib/proxy-env.ts +70 -3
  111. package/src/lib/request-execution-budget.ts +11 -3
  112. package/src/lib/response-body-inactivity.ts +193 -0
  113. package/src/lib/retry-delay.ts +69 -0
  114. package/src/lib/socks5-fetch.ts +631 -0
  115. package/src/lib/spend-reservation-ledger.ts +115 -9
  116. package/src/lib/workflow-budget.ts +145 -8
  117. package/src/oauth/account-quota-rank.ts +72 -15
  118. package/src/oauth/generic-account-failover.ts +40 -27
  119. package/src/oauth/orcarouter.ts +15 -2
  120. package/src/oauth/store.ts +8 -0
  121. package/src/providers/codex-capacity.ts +9 -0
  122. package/src/providers/devin-provider-merge-migration.ts +33 -12
  123. package/src/providers/free-directory.ts +20 -2
  124. package/src/providers/key-failover.ts +261 -7
  125. package/src/providers/model-rename-migration.ts +1 -0
  126. package/src/providers/openai-sidecar.ts +4 -0
  127. package/src/providers/opencode-go-transport.ts +14 -5
  128. package/src/providers/quota/report-cache.ts +3 -0
  129. package/src/providers/registry/entries-extended.ts +96 -0
  130. package/src/providers/registry/model-seeds.ts +78 -21
  131. package/src/responses/apply-patch-envelope.ts +44 -11
  132. package/src/responses/bridge-search-replay-cache.ts +152 -0
  133. package/src/responses/code-mode-helper-compat.ts +26 -16
  134. package/src/responses/custom-tool-compat.ts +1 -1
  135. package/src/responses/hosted-tool-policy.ts +85 -2
  136. package/src/responses/schema.ts +9 -2
  137. package/src/server/auth-cors.ts +26 -0
  138. package/src/server/chat-completions.ts +9 -4
  139. package/src/server/chat-native-sse.ts +26 -9
  140. package/src/server/chat-native.ts +10 -4
  141. package/src/server/claude-messages.ts +24 -2
  142. package/src/server/gui-static.ts +36 -2
  143. package/src/server/inbound-body-admission.ts +187 -0
  144. package/src/server/index.ts +15 -19
  145. package/src/server/management/api-access.ts +3 -4
  146. package/src/server/management/config-routes.ts +31 -6
  147. package/src/server/management/provider-capability-config.ts +35 -7
  148. package/src/server/management/provider-routes.ts +70 -18
  149. package/src/server/proxy-liveness.ts +97 -2
  150. package/src/server/relay.ts +17 -24
  151. package/src/server/request-log.ts +25 -1
  152. package/src/server/responses/adapter-continuation.ts +71 -27
  153. package/src/server/responses/adapter-delivery.ts +39 -8
  154. package/src/server/responses/adapter-dispatch.ts +52 -24
  155. package/src/server/responses/compact.ts +60 -11
  156. package/src/server/responses/core-codex-account.ts +83 -22
  157. package/src/server/responses/core-normalize.ts +12 -5
  158. package/src/server/responses/fetch-helpers.ts +68 -2
  159. package/src/server/responses/passthrough-delivery.ts +10 -1
  160. package/src/server/responses/passthrough-dispatch.ts +113 -48
  161. package/src/server/responses/passthrough-execution.ts +11 -1
  162. package/src/server/responses/request-prepare.ts +29 -0
  163. package/src/server/responses/request-send-budget.ts +84 -7
  164. package/src/server/responses/request-sidecar-auth.ts +16 -8
  165. package/src/server/responses/request-spend.ts +38 -9
  166. package/src/server/responses/request-transport.ts +13 -10
  167. package/src/server/responses/run-turn-execution.ts +20 -5
  168. package/src/server/responses/sidecar-execution.ts +2 -0
  169. package/src/server/responses/ws-upstream.ts +2 -1
  170. package/src/server/responses-custom-tool-repair.ts +2 -2
  171. package/src/server/sse-frame-buffer.ts +12 -10
  172. package/src/server/sse-payload-rewrite.ts +36 -9
  173. package/src/server/system-env-shell.ts +5 -1
  174. package/src/server/system-env.ts +7 -1
  175. package/src/server/workflow-refusal.ts +56 -2
  176. package/src/service/cli.ts +16 -6
  177. package/src/service/guards.ts +10 -0
  178. package/src/service/health.ts +43 -0
  179. package/src/service/state.ts +7 -2
  180. package/src/types/accounts.ts +4 -0
  181. package/src/types/config.ts +100 -3
  182. package/src/types/provider.ts +19 -0
  183. package/src/types/request.ts +7 -1
  184. package/src/types/wire.ts +9 -1
  185. package/src/usage/expected-prices.ts +28 -0
  186. package/src/usage/log.ts +87 -4
  187. package/src/web-search/passthrough-bridge.ts +39 -5
  188. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
@@ -0,0 +1,94 @@
1
+ export const START_USAGE = "Usage: ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]";
2
+ export const START_USAGE_LINE = "ocx start [--port <port>] [--socks5 [host:port] | --socks5-off]";
3
+ export const DEFAULT_SOCKS5_PROXY = "socks5://127.0.0.1:10808";
4
+
5
+ export class StartArgsError extends Error {
6
+ constructor(message: string) {
7
+ super(message);
8
+ this.name = "StartArgsError";
9
+ }
10
+ }
11
+
12
+ export type StartOptions = {
13
+ port?: number;
14
+ /** SOCKS5 URL to persist. */
15
+ socks5?: string;
16
+ socks5Off?: boolean;
17
+ };
18
+
19
+ export function isSocksProxyUrl(proxy: string): boolean {
20
+ return /^socks5h?:\/\//i.test(proxy.trim());
21
+ }
22
+
23
+ export function normalizeSocks5(raw: string): string {
24
+ const trimmed = raw.trim();
25
+ if (!trimmed) return DEFAULT_SOCKS5_PROXY;
26
+ if (isSocksProxyUrl(trimmed)) {
27
+ try {
28
+ const parsed = new URL(trimmed);
29
+ const port = Number(parsed.port);
30
+ if (!parsed.hostname || !parsed.port || !Number.isInteger(port) || port <= 0 || port > 65535) {
31
+ throw new Error("invalid host or port");
32
+ }
33
+ return trimmed;
34
+ } catch {
35
+ throw new StartArgsError("Invalid SOCKS5 address; expected socks5://host:port");
36
+ }
37
+ }
38
+ if (/^socks4a?:\/\//i.test(trimmed)) {
39
+ throw new StartArgsError("Only SOCKS5 proxy URLs are supported");
40
+ }
41
+ if (/^https?:\/\//i.test(trimmed)) {
42
+ throw new StartArgsError("SOCKS5 proxy must be socks5://host:port, not an HTTP URL");
43
+ }
44
+ if (/^\d+$/.test(trimmed)) {
45
+ const port = Number(trimmed);
46
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
47
+ throw new StartArgsError("Invalid SOCKS5 port number");
48
+ }
49
+ return `socks5://127.0.0.1:${port}`;
50
+ }
51
+ const hostPort = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(trimmed);
52
+ if (hostPort) {
53
+ const port = Number(hostPort[2]);
54
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
55
+ throw new StartArgsError("Invalid SOCKS5 port number");
56
+ }
57
+ return `socks5://${trimmed}`;
58
+ }
59
+ throw new StartArgsError("Invalid SOCKS5 address; expected socks5://host:port");
60
+ }
61
+
62
+ export function parseStartOptions(argv: string[]): StartOptions {
63
+ const options: StartOptions = {};
64
+ for (let i = 0; i < argv.length; i++) {
65
+ const arg = argv[i]!;
66
+ if (arg === "--port") {
67
+ const value = argv[++i];
68
+ const port = value && /^\d+$/.test(value) ? Number(value) : NaN;
69
+ if (!Number.isInteger(port) || port <= 0 || port > 65535) {
70
+ throw new StartArgsError("Invalid port number");
71
+ }
72
+ options.port = port;
73
+ continue;
74
+ }
75
+ if (arg === "--socks5") {
76
+ if (options.socks5Off) throw new StartArgsError("--socks5 and --socks5-off cannot be used together");
77
+ const next = argv[i + 1];
78
+ if (next && !next.startsWith("-")) {
79
+ options.socks5 = normalizeSocks5(next);
80
+ i += 1;
81
+ } else {
82
+ options.socks5 = DEFAULT_SOCKS5_PROXY;
83
+ }
84
+ continue;
85
+ }
86
+ if (arg === "--socks5-off") {
87
+ if (options.socks5 !== undefined) throw new StartArgsError("--socks5 and --socks5-off cannot be used together");
88
+ options.socks5Off = true;
89
+ continue;
90
+ }
91
+ throw new StartArgsError(START_USAGE);
92
+ }
93
+ return options;
94
+ }
@@ -21,6 +21,8 @@ const USAGE = `Usage:
21
21
  ocx system codex-app-server [--json]
22
22
  ocx system codex-restart --yes [--json]
23
23
  ocx system codex-cli-update check [--json]
24
+ ocx system codex-cli-update attest [--json]
25
+ ocx system codex-cli-update attest --candidate <absolute-path> --npm-prefix <absolute-path> --npm-cli <absolute-path> --node <absolute-path> [--json]
24
26
  ocx system update check [--channel <latest|preview>] [--json]
25
27
  ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
26
28
  ocx system update status <job-id> [--json]
@@ -74,10 +74,11 @@ export async function handleMachineApi(
74
74
  injected: MachineApiDeps = defaultDeps,
75
75
  ): Promise<Response | null> {
76
76
  const deps = { ...defaultDeps, ...injected };
77
- if (url.pathname === "/api/machine/status" && req.method === "GET") {
77
+ const readMethod = req.method === "GET" || req.method === "HEAD";
78
+ if (url.pathname === "/api/machine/status" && readMethod) {
78
79
  return Response.json(statusPayload(req, state, deps), { headers: { "Cache-Control": "no-store" } });
79
80
  }
80
- if (url.pathname === "/api/machine/clients" && req.method === "GET") {
81
+ if (url.pathname === "/api/machine/clients" && readMethod) {
81
82
  return Response.json({
82
83
  selectedClients: [...state.selectedClients],
83
84
  journalOwner: journalOwner(),
@@ -103,7 +104,7 @@ export async function handleMachineApi(
103
104
  return Response.json({ success: false, error: message }, { status: 502 });
104
105
  }
105
106
  }
106
- if (url.pathname === "/api/machine/shim" && req.method === "GET") {
107
+ if (url.pathname === "/api/machine/shim" && readMethod) {
107
108
  return Response.json(diagnoseCodexShim(), { headers: { "Cache-Control": "no-store" } });
108
109
  }
109
110
  if (url.pathname === "/api/machine/shim" && req.method === "POST") {
@@ -46,9 +46,14 @@ export function machineRouteAllowed(url: URL, req: Request, relayEnabled: boolea
46
46
  if (req.headers.get("upgrade")) return false;
47
47
  const path = url.pathname;
48
48
  if (req.method === "GET" && (path === "/healthz" || path === "/readyz" || path === "/" || path === "/opencodex-session")) return true;
49
- if (req.method === "GET" && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
49
+ if ((req.method === "GET" || req.method === "HEAD") && (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim")) return true;
50
50
  if (req.method === "POST" && (path === "/api/machine/sync" || path === "/api/machine/shim" || path === "/api/machine/disconnect")) return true;
51
51
  if (relayEnabled && path.startsWith("/api/machine/hub-relay/")) return true;
52
+ // Known machine endpoints are admitted for every method so an unsupported
53
+ // method reaches the authenticated method restriction (403) instead of a
54
+ // bare 404 that hides the endpoint entirely.
55
+ if (path === "/api/machine/status" || path === "/api/machine/clients" || path === "/api/machine/shim"
56
+ || path === "/api/machine/sync" || path === "/api/machine/disconnect") return true;
52
57
  if (req.method !== "GET" || path.startsWith("/api/") || path.startsWith("/v1/")) return false;
53
58
  return GUI_SPA_PATHS.has(path)
54
59
  || path.startsWith("/integrations/")
@@ -111,6 +116,14 @@ export function startMachineListener(
111
116
  if (managementPrincipal(req, managementAuth, config) !== "gui-session") {
112
117
  return Response.json({ error: "opencodex machine GUI session required" }, { status: 401 });
113
118
  }
119
+ // A loopback dashboard session proves possession, not user presence: any local
120
+ // process can fetch the dashboard bootstrap and replay its token and CSRF value.
121
+ // Keep the connected listener useful for status/diagnostics, but never let that
122
+ // credentialless bootstrap authorize durable machine changes. Those operations
123
+ // remain available through the explicit CLI commands.
124
+ if (req.method !== "GET" && req.method !== "HEAD") {
125
+ return Response.json({ error: "opencodex machine changes require the local CLI" }, { status: 403 });
126
+ }
114
127
  return await handleMachineApi(req, url, connection, machineApiDeps) ?? json404(req);
115
128
  }
116
129
 
@@ -37,9 +37,8 @@ export const OPENCLAW_API_KEY_ENV_REF = `\${${OPENCLAW_API_KEY_ENV}}`;
37
37
  export const LOOPBACK_API_KEY_PLACEHOLDER = "opencodex-loopback";
38
38
 
39
39
  /**
40
- * Gajae's `apiKeyEnv` is env-name-only and fail-closed. Its sibling `apiKey`
41
- * falls back to treating the literal text as the token when the variable is
42
- * unset, which would silently ship a bogus credential — so we never emit it.
40
+ * Legacy name retained for existing consumers. Gajae exports now use the
41
+ * non-secret loopback placeholder and do not emit an environment reference.
43
42
  */
44
43
  export const GAJAE_API_KEY_ENV = "OPENCODEX_GAJAE_API_KEY";
45
44
 
@@ -41,7 +41,7 @@ export type { RaycastAbility, RaycastAbilityName, RaycastModelEntry, RaycastProv
41
41
  export { buildRaycastClientConfig, summarizeRaycast, buildRaycastContribution } from "./config-export/raycast";
42
42
 
43
43
  import type { OpencodeLaunchEnv, OpencodeCatalogModel, ExportContext, PiModelEntry, ManagedContribution, ManagedFragment, ExportClientId, ExportClientSpec } from "./config-export/contracts";
44
- import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, GAJAE_API_KEY_ENV, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants";
44
+ import { OPENCODE_API_KEY_ENV_REF, OPENCODE_PROVIDER_BLOCK_DEFAULT_CONFIG, OPENCODE_CONFIG_SCHEMA, OPENCODE_PROVIDER_ID, PI_API_DIALECT, LOOPBACK_API_KEY_PLACEHOLDER, HERMES_API_KEY_ENV_REF, OPENCLAW_API_KEY_ENV_REF, OPENCODE_API_KEY_ENV, HERMES_API_KEY_ENV, OPENCLAW_API_KEY_ENV } from "./config-export/constants";
45
45
  import { exportModelLabel, authoritativeContextWindow, outputBudgetFor, normalizeExportModels, inputModalitiesForClient, opencodeModelCapabilities, proxyAdmissionHeaders, singleFragment } from "./config-export/model-metadata";
46
46
  import { buildOmpClientConfig, summarizeOmp, buildOmpContribution } from "./config-export/omp";
47
47
  import { buildDshClientConfig, summarizeDsh, buildDshContribution } from "./config-export/dsh";
@@ -867,7 +867,7 @@ export interface GajaeModelEntry {
867
867
  /** Gajae validates strictly: an unknown field fails the whole config. */
868
868
  export interface GajaeProviderBlock {
869
869
  baseUrl: string;
870
- apiKeyEnv: string;
870
+ apiKey: string;
871
871
  api: "openai-completions";
872
872
  models: GajaeModelEntry[];
873
873
  }
@@ -1056,7 +1056,7 @@ function buildGajaeClientConfig(ctx: ExportContext): GajaeGeneratedConfig {
1056
1056
  providers: {
1057
1057
  [OPENCODE_PROVIDER_ID]: {
1058
1058
  baseUrl: ctx.baseUrl,
1059
- apiKeyEnv: GAJAE_API_KEY_ENV,
1059
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
1060
1060
  api: "openai-completions",
1061
1061
  models,
1062
1062
  },
@@ -1320,8 +1320,8 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1320
1320
  id: "gajae",
1321
1321
  filename: "gajae-models.yaml",
1322
1322
  destination: env => gajaeConfigPath(env),
1323
- apiKeyEnv: GAJAE_API_KEY_ENV,
1324
- exportHint: `export ${GAJAE_API_KEY_ENV}=<your key>`,
1323
+ apiKeyEnv: "",
1324
+ exportHint: "No environment variable is needed for the loopback provider.",
1325
1325
  build: buildGajaeClientConfig,
1326
1326
  format: "yaml",
1327
1327
  summarize: summarizeGajae,
@@ -1,3 +1,4 @@
1
+ import { readOrcaAuthSource } from "./orca-auth-source";
1
2
  import { createHash } from "node:crypto";
2
3
  import { closeSync, existsSync, fstatSync, readFileSync, mkdirSync, openSync, statSync, unlinkSync, writeFileSync } from "node:fs";
3
4
  import { join } from "node:path";
@@ -52,11 +53,20 @@ function isObject(value: unknown): value is Record<string, unknown> {
52
53
  }
53
54
 
54
55
  function isCredential(value: unknown): value is CodexAccountCredentials {
56
+ const hasNoSource = isObject(value)
57
+ && value.sourceAuthPath === undefined
58
+ && value.sourceSubject === undefined;
59
+ const hasCompleteSource = isObject(value)
60
+ && typeof value.sourceAuthPath === "string"
61
+ && value.sourceAuthPath.length > 0
62
+ && typeof value.sourceSubject === "string"
63
+ && value.sourceSubject.length > 0;
55
64
  return isObject(value)
56
65
  && typeof value.accessToken === "string"
57
66
  && typeof value.refreshToken === "string"
58
67
  && typeof value.expiresAt === "number"
59
- && typeof value.chatgptAccountId === "string";
68
+ && typeof value.chatgptAccountId === "string"
69
+ && (hasNoSource || hasCompleteSource);
60
70
  }
61
71
 
62
72
  function isCredentialRecord(value: unknown): value is CodexAccountCredentialRecord {
@@ -78,6 +88,7 @@ export function refreshGrantFingerprintForToken(refreshToken: string): string {
78
88
  }
79
89
 
80
90
  function recordGrantFingerprint(record: CodexAccountCredentialRecord): string | undefined {
91
+ if (record.credential?.sourceAuthPath) return undefined;
81
92
  return record.refreshGrantFingerprint ?? (
82
93
  record.credential ? refreshGrantFingerprintForToken(record.credential.refreshToken) : undefined
83
94
  );
@@ -426,7 +437,7 @@ export function commitRefreshedCodexCredentialWithAliases(
426
437
  return withCredentialMutationLockSync(() => {
427
438
  const store = loadCodexAccountRecordStore();
428
439
  const current = store[id];
429
- if (!current || current.generation !== generation || current.deletedAt != null || !current.credential) {
440
+ if (!current || current.generation !== generation || current.deletedAt != null || !current.credential || current.credential.sourceAuthPath) {
430
441
  return { committed: false, propagatedAliases: [] };
431
442
  }
432
443
  const priorCredential = current.credential;
@@ -458,7 +469,7 @@ export function commitRefreshedCodexCredentialWithAliases(
458
469
  && priorCredential.chatgptAccountId === cred.chatgptAccountId
459
470
  ) {
460
471
  for (const [aliasId, alias] of Object.entries(store)) {
461
- if (aliasId === id || alias.deletedAt != null || !alias.credential) continue;
472
+ if (aliasId === id || alias.deletedAt != null || !alias.credential || alias.credential.sourceAuthPath) continue;
462
473
  if (recordGrantFingerprint(alias) !== priorFingerprint) continue;
463
474
  if (alias.credential.accessToken !== priorCredential.accessToken) continue;
464
475
  if (alias.credential.expiresAt !== priorCredential.expiresAt) continue;
@@ -630,7 +641,14 @@ export type CodexRefreshProvenance = "self-refresh" | "joined-lineage" | "extern
630
641
 
631
642
  /** Terminal outcome of one forced refresh, as seen by the caller that requested it. */
632
643
  export type ForcedRefreshOutcome =
633
- | { kind: "resolved"; provenance: CodexRefreshProvenance; generation: number; rotated: boolean }
644
+ | {
645
+ kind: "resolved";
646
+ provenance: CodexRefreshProvenance;
647
+ generation: number;
648
+ rotated: boolean;
649
+ /** Same-grant records advanced by this refresh, at their committed generations. */
650
+ propagatedAliases?: { id: string; generation: number }[];
651
+ }
634
652
  | { kind: "failed"; error: unknown };
635
653
  const MAX_CODEX_REFRESH_FLIGHTS = 32;
636
654
  const CODEX_REFRESH_FLIGHT_STALE_MS = 120_000;
@@ -776,7 +794,7 @@ function findFreshCredentialForGrant(
776
794
  // require both ids to be present and exactly equal rather than inferring identity from the grant.
777
795
  if (!expectedChatgptAccountId) return null;
778
796
  for (const [candidateId, candidate] of Object.entries(records)) {
779
- if (candidateId === excludeId || candidate.deletedAt != null || !candidate.credential) continue;
797
+ if (candidateId === excludeId || candidate.deletedAt != null || !candidate.credential || candidate.credential.sourceAuthPath) continue;
780
798
  if (recordGrantFingerprint(candidate) !== refreshGrantFingerprint) continue;
781
799
  if (!candidate.credential.chatgptAccountId) continue;
782
800
  if (candidate.credential.chatgptAccountId !== expectedChatgptAccountId) continue;
@@ -926,6 +944,9 @@ export async function forceRefreshCodexPoolToken(
926
944
  provenance: classify(resolved),
927
945
  generation: resolved.generation,
928
946
  rotated: resolved.accessToken !== options.rejectedAccessToken,
947
+ ...(resolved.propagatedAliases?.length
948
+ ? { propagatedAliases: resolved.propagatedAliases }
949
+ : {}),
929
950
  });
930
951
  },
931
952
  error => {
@@ -967,6 +988,57 @@ export async function getValidCodexToken(
967
988
  };
968
989
  }
969
990
 
991
+ /** Source credentials never join refresh flights or spend a refresh grant, including after 401. */
992
+ function resolveOrcaSourceToken(id: string, forced?: ForcedRefreshFence): CodexRefreshResult {
993
+ const initial = readCodexAccountRecord(id);
994
+ const initialCredential = initial?.credential;
995
+ if (!initial || initial.deletedAt != null || !initialCredential?.sourceAuthPath || !initialCredential.sourceSubject) {
996
+ throw new CodexCredentialGenerationConflictError();
997
+ }
998
+ const sourceCredential = readOrcaAuthSource(initialCredential.sourceAuthPath);
999
+ if (sourceCredential.chatgptAccountId !== initialCredential.chatgptAccountId
1000
+ || sourceCredential.sourceSubject !== initialCredential.sourceSubject) {
1001
+ throw new Error("Orca credential identity changed; reimport the account explicitly.");
1002
+ }
1003
+ if (sourceCredential.accessToken === initialCredential.accessToken
1004
+ && sourceCredential.expiresAt === initialCredential.expiresAt) {
1005
+ if (forced?.rejectedAccessToken === sourceCredential.accessToken) {
1006
+ throw new Error("Orca bearer was rejected; update the account in Orca and retry.");
1007
+ }
1008
+ return { accessToken: sourceCredential.accessToken, chatgptAccountId: sourceCredential.chatgptAccountId,
1009
+ generation: initial.generation, provenance: "external-replacement" };
1010
+ }
1011
+
1012
+ return withCredentialMutationLockSync(() => {
1013
+ const store = loadCodexAccountRecordStore();
1014
+ const record = store[id];
1015
+ const prior = record?.credential;
1016
+ if (!record || record.deletedAt != null || !prior?.sourceAuthPath || !prior.sourceSubject) {
1017
+ throw new CodexCredentialGenerationConflictError();
1018
+ }
1019
+ if (record.generation !== initial.generation) throw new CodexCredentialGenerationConflictError();
1020
+ const credential = readOrcaAuthSource(prior.sourceAuthPath);
1021
+ if (credential.chatgptAccountId !== prior.chatgptAccountId || credential.sourceSubject !== prior.sourceSubject) {
1022
+ throw new Error("Orca credential identity changed; reimport the account explicitly.");
1023
+ }
1024
+ if (credential.accessToken !== prior.accessToken || credential.expiresAt !== prior.expiresAt) {
1025
+ store[id] = {
1026
+ credential, generation: record.generation + 1, replacedAt: Date.now(),
1027
+ // The source identity was checked above. Retire old-generation writers without
1028
+ // splitting this same account's retained quota history on every source rotation.
1029
+ quotaHistoryIdentity: record.quotaHistoryIdentity,
1030
+ ...preservedValidationMetadata(record),
1031
+ };
1032
+ persistCredentialMutation(store);
1033
+ }
1034
+ if (forced?.rejectedAccessToken === credential.accessToken) {
1035
+ throw new Error("Orca bearer was rejected; update the account in Orca and retry.");
1036
+ }
1037
+ return { accessToken: credential.accessToken, chatgptAccountId: credential.chatgptAccountId,
1038
+ generation: store[id]!.generation, provenance: "external-replacement" };
1039
+ });
1040
+ }
1041
+
970
1042
  async function resolveCodexToken(
971
1043
  id: string,
972
1044
  forced?: ForcedRefreshFence,
@@ -976,6 +1048,7 @@ async function resolveCodexToken(
976
1048
  const record = readCodexAccountRecord(id);
977
1049
  const cred = record?.deletedAt == null ? record?.credential : undefined;
978
1050
  if (!record || !cred) throw new CodexCredentialUnavailableError();
1051
+ if (cred.sourceAuthPath) return resolveOrcaSourceToken(id, forced);
979
1052
  const refreshGrantFingerprint = recordGrantFingerprint(record);
980
1053
  if (!refreshGrantFingerprint) throw new CodexCredentialUnavailableError();
981
1054
 
@@ -1005,6 +1078,7 @@ async function resolveCodexToken(
1005
1078
  const refreshed = await awaitOwnCancellation(existing.promise, callerSignal);
1006
1079
  const current = readCodexAccountRecord(id);
1007
1080
  const currentCred = current?.deletedAt == null ? current?.credential : undefined;
1081
+ if (currentCred?.sourceAuthPath) return resolveOrcaSourceToken(id, forced);
1008
1082
  // The flight owner already committed this credential, and it is the one stored
1009
1083
  // for this account: adopt the stored state instead of CAS-writing the identical
1010
1084
  // bytes, which would bump the generation a second time and invalidate the
@@ -1126,6 +1200,7 @@ async function resolveCodexToken(
1126
1200
  const lockedRecord = readCodexAccountRecord(id);
1127
1201
  const lockedCred = lockedRecord?.deletedAt == null ? lockedRecord?.credential : undefined;
1128
1202
  if (!lockedRecord || !lockedCred) throw new CodexCredentialGenerationConflictError();
1203
+ if (lockedCred.sourceAuthPath) return resolveOrcaSourceToken(id, forced);
1129
1204
  const startGeneration = lockedRecord.generation;
1130
1205
  const lockedRefreshGrantFingerprint = recordGrantFingerprint(lockedRecord);
1131
1206
  if (lockedRefreshGrantFingerprint !== refreshGrantFingerprint) {
@@ -1331,6 +1406,7 @@ async function resolveCodexToken(
1331
1406
  // Provenance rides out with the rest: a joiner that adopts this result needs the
1332
1407
  // flight's own classification, not a guess made at the adoption site (#3019).
1333
1408
  ...(result.provenance !== undefined ? { provenance: result.provenance } : {}),
1409
+ ...(result.propagatedAliases?.length ? { propagatedAliases: result.propagatedAliases } : {}),
1334
1410
  ...(result.resolvedGrantFingerprint !== undefined
1335
1411
  ? { resolvedGrantFingerprint: result.resolvedGrantFingerprint }
1336
1412
  : {}),
@@ -9,7 +9,7 @@ import { codexWarmupFailureReason, warmCodexAccount } from "../warmup";
9
9
  import { readBoundedResponseBody } from "../../lib/bounded-body";
10
10
  import { ResourceAdmissionError } from "../../lib/admission";
11
11
  import { WHAM_REQUEST_TIMEOUT_MS } from "../quota-recovery-timing";
12
- import { claimQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
12
+ import { claimQuotaRecovery, fencePropagatedQuotaRecovery, quotaRecoveryTerminalFor, releaseQuotaRecovery, settleQuotaRecovery, settleQuotaRecoveryTerminal } from "../quota-401-recovery";
13
13
  import { seedLoginRowsForTests } from "./login-state";
14
14
  import { nonEmptyPlan } from "./runtime-config";
15
15
 
@@ -205,6 +205,12 @@ export async function recoverPoolQuotaFrom401(ctx: {
205
205
  onSettled: outcome => {
206
206
  if (outcome.kind === "resolved") {
207
207
  settleQuotaRecovery(accountId, claim.claimId, outcome);
208
+ // A propagated alias adopted the rotated grant without spending its own claim. Fence
209
+ // each committed alias generation so a later 401 on it cannot claim a second refresh
210
+ // of the same underlying grant.
211
+ for (const alias of outcome.propagatedAliases ?? []) {
212
+ fencePropagatedQuotaRecovery(alias.id, alias.generation);
213
+ }
208
214
  } else if (outcome.error instanceof TokenRefreshError && isTerminalRefreshError(outcome.error)) {
209
215
  // A revoked or expired grant does not become valid on the next poll. Releasing it
210
216
  // into backoff would let the following bare 401 find a non-terminal record and
@@ -476,9 +482,14 @@ export async function fetchPoolAccountQuota(
476
482
  && generation !== undefined && record.generation === generation
477
483
  && isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? configuredPlan)) {
478
484
  try {
485
+ // Quota I/O may outlive the source capture. Never validate a rotated or revoked link
486
+ // using an older generation's observation. Explicit validation consent still applies.
487
+ const sourceToken = record.credential.sourceAuthPath ? await getValidToken(accountId) : null;
488
+ if (sourceToken && (sourceToken.generation !== generation
489
+ || !isCodexAccountGenerationLive(accountId, generation))) return result;
479
490
  await warmCodexAccount({
480
- accessToken: record.credential.accessToken,
481
- chatgptAccountId: record.credential.chatgptAccountId,
491
+ accessToken: sourceToken?.accessToken ?? record.credential.accessToken,
492
+ chatgptAccountId: sourceToken?.chatgptAccountId ?? record.credential.chatgptAccountId,
482
493
  });
483
494
  markCodexAccountValidated(accountId, Date.now(), generation);
484
495
  clearAccountNeedsReauth(accountId, generation);
@@ -7,7 +7,7 @@ import { getAccountQuotaHistory, listAccountQuotas } from "../quota";
7
7
  import { deleteCodexAccount } from "../account-lifecycle";
8
8
  import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
9
9
  import { clearCodexAccountPin, isCodexAccountPriorityKey, pinnedCodexAccountId, setCodexAccountPin, setCodexAccountPriority } from "../account-priority";
10
- import { codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
10
+ import { codexAccountPinDrainReason, codexQuotaScopeForModel, clearCodexAccountCooldown, clearThreadAccountMapForAccount, getEffectiveActiveCodexAccountId, isEffectiveCodexAccountPinned, resetCodexRoutingForManualSelection } from "../routing";
11
11
  import { DEFAULT_ACCOUNT_PRIORITY, MAX_ACCOUNT_PRIORITY, MIN_ACCOUNT_PRIORITY, normalizeAccountPoolStickyLimit, normalizeCodexAccountPoolStrategy, parseAccountPoolStickyLimit, parseCodexAccountPoolStrategy, parseAccountPriority } from "../pool-rotation";
12
12
  import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
13
13
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
@@ -235,7 +235,22 @@ export async function handleCodexAuthAPI(
235
235
  else setCodexAccountPin(runtimeConfig, targetAccountId);
236
236
  resetCodexRoutingForManualSelection(targetAccountId);
237
237
  saveRuntimeConfig(config, runtimeConfig);
238
- return jsonResponse({ ok: true, activeCodexAccountId: body.accountId, appliesImmediately: true });
238
+ // A pin this route accepts can still be dropped by the very next resolve, and saying
239
+ // nothing about that is what made the setting look ignored (#4521). The checks above
240
+ // refuse an account that cannot be selected at all; this reports the one remaining
241
+ // outcome they do not cover, from the same predicate routing releases on, so the two
242
+ // cannot drift. Absent means the pin survives — additive for existing clients.
243
+ // `appliesImmediately` is unchanged: it answers whether thread affinity was cleared,
244
+ // not whether the pin is durable.
245
+ const pinDrainReason = body.accountId == null
246
+ ? undefined
247
+ : codexAccountPinDrainReason(runtimeConfig, targetAccountId);
248
+ return jsonResponse({
249
+ ok: true,
250
+ activeCodexAccountId: body.accountId,
251
+ appliesImmediately: true,
252
+ ...(pinDrainReason !== undefined ? { pinDrained: true, pinDrainReason } : {}),
253
+ });
239
254
  }
240
255
 
241
256
  if (url.pathname === "/api/codex-auth/active" && req.method === "GET") {
@@ -135,20 +135,22 @@ export function requestOwnedMainPinState(
135
135
  }
136
136
 
137
137
  /**
138
- * Every thread keys as ITSELF, never as its parent (#4546, wp8).
138
+ * One conversation tree keys as ONE cohort (#4780).
139
139
  *
140
- * The old rule preferred `x-codex-parent-thread-id`, so every child of one parent bound under
141
- * the RAW parent id -- one shared entry, unrelated to the root's own `app:HMAC(session, thread)`
142
- * binding -- and a grandchild keyed on its own parent landed on a key nobody had ever bound.
143
- * A child therefore started cold while its parent was being served warm somewhere, and no
144
- * child could hold a binding of its own.
140
+ * Upstream keys its prompt cache on something the whole tree shares -- `prompt_cache_key` is the
141
+ * session id, or `{source}:{parent_thread_id}` for an internal session -- and one `AgentControl`
142
+ * whose `session_id` is the root thread's id is shared with every sub-agent. While the proxy
143
+ * keyed per thread, two requests could carry an identical `prompt_cache_key` and be served by
144
+ * different accounts, so the split member asserted a warm prefix that was deterministically cold
145
+ * on its account. Nothing failed; the prompt was replayed in full and the tokens burned.
145
146
  *
146
- * Now a request with a `thread-id` keys as HMAC(session ?? parent, thread). A root is
147
- * unchanged, a child gets an independent key, and a request naming only a parent rides the
148
- * parent's lane under HMAC(parent, parent) -- the same one-to-one lane it always had, minus
149
- * the caller-supplied identifier that used to sit in Pool state. Which requests produce no
150
- * key at all is unchanged. First placement for a child is what consults the family, through
151
- * `recordCodexThreadLineage` below and the placement hook in ./routing.
147
+ * THIS IS NOT A REVERT OF #4546 wp8. wp8 fixed a different defect -- a child bound under the RAW
148
+ * parent id, an identity unrelated to the root's own binding, so siblings shared an entry the
149
+ * root was not on and a grandchild landed on a key nobody had bound. A cohort key has no such
150
+ * incoherence, because the root's own binding IS the cohort key. What wp8 additionally gave each
151
+ * thread, a binding of its own, is what this deliberately gives up.
152
+ *
153
+ * Which requests produce no key at all is unchanged, through both units.
152
154
  *
153
155
  * The derivation itself lives in ./lineage so a lineage record's conversation key and the key
154
156
  * the thread actually binds under can never drift apart.
@@ -1419,6 +1421,7 @@ export function materializeCodexUpstreamAuth(
1419
1421
  if (!stored?.accessToken || !isMainAccountTokenLive()) {
1420
1422
  throw new CodexMainSubstitutionUnavailableError();
1421
1423
  }
1424
+ selected.delete("chatgpt-account-id");
1422
1425
  selected.set("authorization", `Bearer ${stored.accessToken}`);
1423
1426
  if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
1424
1427
  observeSelectedMainCredential(stored, writer);
@@ -1496,6 +1499,7 @@ export async function materializeCodexUpstreamAuthAsync(
1496
1499
  ...(options.nativeMainRefreshDependencies ?? {}),
1497
1500
  });
1498
1501
  if (!stored?.accessToken) throw new CodexMainSubstitutionUnavailableError();
1502
+ selected.delete("chatgpt-account-id");
1499
1503
  selected.set("authorization", `Bearer ${stored.accessToken}`);
1500
1504
  if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
1501
1505
  observeSelectedMainCredential(stored, writer);
@@ -26,7 +26,7 @@ import {
26
26
  } from "./metadata";
27
27
  import { resetBundledCatalogCacheForTests } from "./bundled";
28
28
  import { isMultiAgentV2Enabled } from "../features";
29
- import { ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
29
+ import { clampedDefaultEffort, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
30
30
  import { clearGatherRoutedModelsInflight, lastDropWarnSignature } from "./provider-fetch";
31
31
  import {
32
32
  accountSelectorShadowCollisionWarnings,
@@ -490,10 +490,14 @@ export interface ObservedCatalogMergeInput {
490
490
  readonly includeNativeOpenAi: boolean;
491
491
  readonly accountBoundEntries: readonly RawEntry[];
492
492
  readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
493
+ /** Routed slugs that must not gain a missing synthetic max rung during retained-row repair. */
494
+ readonly suppressedSyntheticMaxSlugs?: ReadonlySet<string>;
493
495
  readonly policy: ObservedCatalogMergePolicy;
494
496
  readonly openaiContextCap?: NativeContextLimitsInput;
495
497
  /** Exact display-only labels for bare native OpenAI models. */
496
498
  readonly nativeDisplayNames?: Readonly<Record<string, string>>;
499
+ /** Pristine installed-catalog multi-agent pins; see MultiAgentModeOptions.nativeDefaults. */
500
+ readonly nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>;
497
501
  }
498
502
 
499
503
  /**
@@ -526,9 +530,11 @@ export function mergeCatalogEntriesFromObservedState({
526
530
  includeNativeOpenAi,
527
531
  accountBoundEntries,
528
532
  suppressedBareNativeSlugs = new Set(),
533
+ suppressedSyntheticMaxSlugs = new Set(),
529
534
  policy,
530
535
  openaiContextCap,
531
536
  nativeDisplayNames,
537
+ nativeMultiAgentDefaults,
532
538
  }: ObservedCatalogMergeInput): RawEntry[] {
533
539
  // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
534
540
  // the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
@@ -860,16 +866,29 @@ export function mergeCatalogEntriesFromObservedState({
860
866
  });
861
867
  // Mock-max universality (260709): preserved routed entries from disk may predate
862
868
  // the max rung — ensure it here so subagent max spawns validate on every
863
- // reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
869
+ // reasoning-capable entry. A suppressed preserved row that already contains max keeps it;
870
+ // without persisted provenance, only a healthy provider rebuild can distinguish and remove
871
+ // an older synthetic rung from a real provider-declared rung. max only: 5.6 exact ladders
872
+ // (luna: no ultra) stay intact.
864
873
  if (!freshCustomEntries.has(m) && !exactCombo && !reserveProjection && !String(e.slug ?? "").startsWith("opencode-go/")) {
865
874
  const levels = Array.isArray(e.supported_reasoning_levels)
866
875
  ? e.supported_reasoning_levels as Array<{ effort?: string }>
867
876
  : [];
868
- if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
877
+ if (levels.length > 0
878
+ && !suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
879
+ && !levels.some(level => level.effort === "max")) {
869
880
  levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
870
881
  ?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
871
882
  e.supported_reasoning_levels = levels;
872
883
  }
884
+ if (suppressedSyntheticMaxSlugs.has(String(e.slug ?? ""))
885
+ && typeof e.default_reasoning_level === "string"
886
+ && !levels.some(level => level.effort === e.default_reasoning_level)) {
887
+ e.default_reasoning_level = clampedDefaultEffort(
888
+ e.default_reasoning_level,
889
+ levels.flatMap(level => typeof level.effort === "string" ? [level.effort] : []),
890
+ );
891
+ }
873
892
  }
874
893
  if (wsEnabled) e.supports_websockets = true;
875
894
  else {
@@ -886,7 +905,7 @@ export function mergeCatalogEntriesFromObservedState({
886
905
  applyNativeVisibility(mergedEntries, disabledModels, alignedAccountBoundEntries.length > 0, observedNativeSlugs),
887
906
  multiAgentMode,
888
907
  multiAgentV2Enabled,
889
- { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection },
908
+ { keepNativeChatGptOnV1, preserveDefaultMultiAgentVersion: isReserveCatalogProjection, nativeDefaults: nativeMultiAgentDefaults },
890
909
  );
891
910
  applyFullModelPickerOrder(versionedEntries, modelPickerOrder);
892
911
  for (const entry of versionedEntries) {
@@ -937,6 +956,7 @@ export function mergeCatalogEntriesForSync(
937
956
  ),
938
957
  openaiContextCap?: NativeContextLimitsInput,
939
958
  keepNativeChatGptOnV1 = false,
959
+ nativeMultiAgentDefaults?: ReadonlyMap<string, string | null>,
940
960
  ): RawEntry[] {
941
961
  // Retained for source compatibility with the original helper contract. Raw provider ids must
942
962
  // not suppress same-named native rows; actual admitted combo entries own that decision now.
@@ -973,6 +993,7 @@ export function mergeCatalogEntriesForSync(
973
993
  accountBoundEntries,
974
994
  suppressedBareNativeSlugs,
975
995
  openaiContextCap,
996
+ nativeMultiAgentDefaults,
976
997
  policy: {
977
998
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
978
999
  warningPolicy: "emit",
@@ -160,6 +160,7 @@ export function deriveEntry(
160
160
  preserveExactReasoning
161
161
  || codexForwardNativeCapabilityAlias !== null
162
162
  || preservePinnedNativeCustomReasoning(model),
163
+ model?.suppressSyntheticMax === true,
163
164
  );
164
165
  // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
165
166
  // native tool/search/responses-lite contract while preserving the routed slug and wire id.
@@ -208,7 +209,13 @@ export function deriveEntry(
208
209
  };
209
210
  if (isRouted) {
210
211
  applyRoutedCodexToolMode(entry, model?.codexToolMode);
211
- applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExactReasoning || preservePinnedNativeCustomReasoning(model));
212
+ applyReasoningLevels(
213
+ entry,
214
+ model?.reasoningEfforts,
215
+ model?.defaultReasoningEffort,
216
+ preserveExactReasoning || preservePinnedNativeCustomReasoning(model),
217
+ model?.suppressSyntheticMax === true,
218
+ );
212
219
  }
213
220
  else {
214
221
  applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);