@yansigit/opencodex 2.33.1 → 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 (177) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-BjCaHxdz.js +112 -0
  3. package/gui/dist/assets/index-DLkXOXLC.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -3
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +40 -3
  8. package/src/adapters/cursor/call-id.ts +44 -0
  9. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  10. package/src/adapters/cursor/discovery.ts +60 -2
  11. package/src/adapters/cursor/effort-map.ts +79 -1
  12. package/src/adapters/cursor/envelope-echo.ts +9 -9
  13. package/src/adapters/cursor/live-models.ts +7 -2
  14. package/src/adapters/cursor/live-transport.ts +14 -0
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-shell.ts +103 -6
  17. package/src/adapters/cursor/native-exec.ts +20 -0
  18. package/src/adapters/cursor/protobuf-request.ts +74 -39
  19. package/src/adapters/cursor/request-builder.ts +32 -10
  20. package/src/adapters/cursor/tool-definitions.ts +13 -10
  21. package/src/adapters/cursor/tool-result-normalize.ts +35 -3
  22. package/src/adapters/cursor/types.ts +16 -4
  23. package/src/adapters/cursor.ts +97 -70
  24. package/src/adapters/google-antigravity-replay.ts +105 -25
  25. package/src/adapters/google-antigravity-wire.ts +5 -0
  26. package/src/adapters/google-errors.ts +41 -12
  27. package/src/adapters/google-http.ts +12 -11
  28. package/src/adapters/google.ts +156 -65
  29. package/src/adapters/image.ts +1 -1
  30. package/src/adapters/kiro-constants.ts +15 -0
  31. package/src/adapters/kiro-tools.ts +43 -15
  32. package/src/adapters/kiro.ts +54 -9
  33. package/src/adapters/openai-chat.ts +286 -242
  34. package/src/adapters/openai-responses.ts +334 -24
  35. package/src/adapters/run-turn-queue.ts +36 -1
  36. package/src/adapters/tool-catalog-nudge.ts +2 -2
  37. package/src/adapters/xai-tool-schema.ts +436 -0
  38. package/src/bridge.ts +67 -26
  39. package/src/chat/inbound.ts +14 -1
  40. package/src/chat/outbound.ts +15 -7
  41. package/src/claude/agents-inject.ts +8 -1
  42. package/src/claude/outbound.ts +10 -8
  43. package/src/cli/account-api.ts +27 -7
  44. package/src/cli/account-extended.ts +10 -3
  45. package/src/cli/account.ts +29 -5
  46. package/src/cli/alias.ts +66 -0
  47. package/src/cli/claude.ts +26 -1
  48. package/src/cli/dispatch.ts +13 -1
  49. package/src/cli/help.ts +1 -0
  50. package/src/cli/index.ts +5 -0
  51. package/src/cli/init.ts +1 -0
  52. package/src/cli/models-runtime.ts +95 -0
  53. package/src/cli/models.ts +13 -7
  54. package/src/cli/provider-runtime.ts +16 -2
  55. package/src/cli/registry.ts +6 -1
  56. package/src/cli/telemetry-commands.ts +25 -0
  57. package/src/cli/v2.ts +34 -10
  58. package/src/codex/account-pause.ts +2 -1
  59. package/src/codex/account-priority.ts +3 -2
  60. package/src/codex/app-server-processes.ts +80 -6
  61. package/src/codex/auth-api.ts +48 -8
  62. package/src/codex/auth-context.ts +21 -18
  63. package/src/codex/catalog/aggregation.ts +6 -0
  64. package/src/codex/catalog/model-metadata.ts +13 -1
  65. package/src/codex/catalog/native-models.ts +5 -2
  66. package/src/codex/catalog/parsing.ts +16 -0
  67. package/src/codex/catalog/provider-fetch.ts +20 -37
  68. package/src/codex/catalog/sync.ts +127 -2
  69. package/src/codex/catalog.ts +1 -1
  70. package/src/codex/codex-write-lock.ts +3 -1
  71. package/src/codex/convergence-types.ts +1 -1
  72. package/src/codex/convergence.ts +22 -2
  73. package/src/codex/desired-state.ts +2 -2
  74. package/src/codex/desktop-app-restart.ts +18 -5
  75. package/src/codex/inject-coordination.ts +83 -0
  76. package/src/codex/inject.ts +14 -1
  77. package/src/codex/log-guard/inspect.ts +22 -4
  78. package/src/codex/model-entitlements.ts +9 -2
  79. package/src/codex/prompt-layers.ts +371 -25
  80. package/src/codex/prompt-text-probe.ts +238 -0
  81. package/src/codex/quota.ts +123 -18
  82. package/src/codex/routing.ts +9 -0
  83. package/src/codex/subagent-model-fallback.ts +198 -27
  84. package/src/codex/transition-state.ts +107 -8
  85. package/src/combos/types.ts +10 -0
  86. package/src/compatibility/openai-responses.ts +33 -1
  87. package/src/config/autonomous-remediation.ts +21 -0
  88. package/src/config/provider-validation.ts +14 -0
  89. package/src/config/rebase-provenance.ts +68 -0
  90. package/src/config.ts +191 -17
  91. package/src/generated/compatibility-version.json +229 -153
  92. package/src/images/loop.ts +5 -4
  93. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  94. package/src/lab/fabric/producer-child.ts +1 -1
  95. package/src/lib/config-ownership.ts +20 -0
  96. package/src/lib/errors.ts +11 -2
  97. package/src/lib/package-tree-integrity.ts +101 -0
  98. package/src/oauth/aistudio-credentials.ts +65 -0
  99. package/src/oauth/aistudio-native-daemon.ts +54 -0
  100. package/src/oauth/generic-account-failover.ts +231 -0
  101. package/src/oauth/index.ts +57 -5
  102. package/src/oauth/key-providers.ts +10 -1
  103. package/src/oauth/kiro.ts +45 -0
  104. package/src/oauth/login-cli.ts +19 -20
  105. package/src/oauth/types.ts +15 -0
  106. package/src/providers/codex-capacity.ts +5 -2
  107. package/src/providers/command-code-efforts.ts +38 -6
  108. package/src/providers/context-cap.ts +4 -3
  109. package/src/providers/default-aliases.ts +65 -0
  110. package/src/providers/derive.ts +28 -0
  111. package/src/providers/fastwire.ts +7 -1
  112. package/src/providers/model-presets.ts +119 -0
  113. package/src/providers/new-model-policy.ts +146 -0
  114. package/src/providers/provider-id-rewrite.ts +2 -1
  115. package/src/providers/quota.ts +73 -14
  116. package/src/providers/registry.ts +160 -68
  117. package/src/providers/slug-codec.ts +52 -0
  118. package/src/responses/code-mode-helper-compat.ts +50 -0
  119. package/src/responses/custom-tool-compat.ts +34 -10
  120. package/src/responses/parser.ts +4 -0
  121. package/src/responses/schema.ts +5 -1
  122. package/src/responses/thought-signature-replay.ts +17 -0
  123. package/src/router.ts +40 -2
  124. package/src/routing/analytics.ts +1 -0
  125. package/src/routing/quota.ts +10 -0
  126. package/src/server/auth-cors.ts +23 -0
  127. package/src/server/chat-completions.ts +24 -16
  128. package/src/server/chat-native-sse.ts +3 -3
  129. package/src/server/chat-native.ts +30 -11
  130. package/src/server/claude-messages.ts +1 -1
  131. package/src/server/effort-policy.ts +16 -0
  132. package/src/server/index.ts +152 -80
  133. package/src/server/lifecycle.ts +52 -1
  134. package/src/server/management/agent-settings-routes.ts +31 -15
  135. package/src/server/management/codex-prompt-routes.ts +570 -0
  136. package/src/server/management/combo-routes.ts +2 -1
  137. package/src/server/management/config-routes.ts +27 -9
  138. package/src/server/management/context.ts +9 -0
  139. package/src/server/management/logs-usage-routes.ts +1 -1
  140. package/src/server/management/model-routes.ts +266 -0
  141. package/src/server/management/provider-routes.ts +137 -3
  142. package/src/server/management/routing-profile-routes.ts +2 -2
  143. package/src/server/management-api.ts +2 -0
  144. package/src/server/relay-eager.ts +147 -20
  145. package/src/server/relay.ts +251 -19
  146. package/src/server/request-log-conversation.ts +21 -0
  147. package/src/server/request-log.ts +46 -20
  148. package/src/server/responses/collaboration.ts +42 -5
  149. package/src/server/responses/combo-stream-preflight.ts +10 -3
  150. package/src/server/responses/core.ts +571 -137
  151. package/src/server/responses/empty-completion-guard.ts +35 -0
  152. package/src/server/responses/fetch-helpers.ts +14 -6
  153. package/src/server/responses/input-admission.ts +3 -1
  154. package/src/server/responses/passthrough-error.ts +33 -9
  155. package/src/server/responses/responses-field-backfill.ts +105 -13
  156. package/src/server/responses/ws-upstream.ts +35 -5
  157. package/src/server/responses-custom-tool-repair.ts +52 -7
  158. package/src/server/responses-terminal-repair.ts +25 -4
  159. package/src/server/sse-frame-buffer.ts +31 -4
  160. package/src/server/ws-bridge.ts +15 -4
  161. package/src/telemetry/dispatcher.ts +44 -0
  162. package/src/telemetry/fingerprint.ts +24 -0
  163. package/src/telemetry/hook.ts +43 -0
  164. package/src/telemetry/ledger.ts +54 -0
  165. package/src/telemetry/types.ts +23 -0
  166. package/src/types/config.ts +66 -14
  167. package/src/types/provider.ts +77 -0
  168. package/src/types/request.ts +16 -10
  169. package/src/types/tools.ts +20 -25
  170. package/src/types.ts +1 -0
  171. package/src/usage/expected-prices.ts +43 -24
  172. package/src/usage/log.ts +17 -7
  173. package/src/web-search/exa-executor.ts +40 -9
  174. package/src/web-search/loop.ts +5 -4
  175. package/gui/dist/assets/index-CIDo4y4k.js +0 -102
  176. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
  177. package/src/server/aistudio-ws-hub.ts +0 -295
@@ -0,0 +1,50 @@
1
+ import { normalizeApplyPatchDelimiters } from "./apply-patch-envelope";
2
+
3
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
4
+ return !!value && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+
7
+ function unwrapPatchInput(value: string): string {
8
+ try {
9
+ const parsed: unknown = JSON.parse(value);
10
+ if (isPlainObject(parsed)) {
11
+ if (typeof parsed.input === "string") return parsed.input;
12
+ if (typeof parsed.patch === "string") return parsed.patch;
13
+ }
14
+ } catch {
15
+ // Native custom calls carry the patch body directly.
16
+ }
17
+ return value;
18
+ }
19
+
20
+ /**
21
+ * Convert a nested Code Mode helper call into unified-exec JavaScript.
22
+ *
23
+ * Parsed values are serialized as data, never interpolated as source, so command and patch text
24
+ * cannot escape the generated call. Invalid structured shell payloads are also passed as data so
25
+ * nested-tool validation can reject them without evaluating provider text as JavaScript.
26
+ */
27
+ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
28
+ if (typeof argumentsText !== "string") return "";
29
+ if (toolName === "apply_patch") {
30
+ const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText));
31
+ return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
32
+ }
33
+ let parsed: unknown = argumentsText;
34
+ try {
35
+ parsed = JSON.parse(argumentsText);
36
+ } catch {
37
+ // Keep malformed provider text as data rather than executable source.
38
+ }
39
+ const args: unknown = isPlainObject(parsed) ? { ...parsed } : parsed;
40
+ if (
41
+ toolName === "shell_command"
42
+ && isPlainObject(args)
43
+ && typeof args.command === "string"
44
+ && args.cmd === undefined
45
+ ) {
46
+ args.cmd = args.command;
47
+ delete args.command;
48
+ }
49
+ return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
50
+ }
@@ -1,9 +1,10 @@
1
- import { namespacedToolName } from "../types";
1
+ import { namespacedToolName, normalizeDeclaredToolName } from "../types";
2
2
  import {
3
3
  normalizeApplyPatchDelimiters,
4
4
  repairFreeformToolInput,
5
5
  unwrapFreeformToolInput,
6
6
  } from "./apply-patch-envelope";
7
+ import { compileCodeModeHelperInput } from "./code-mode-helper-compat";
7
8
  import { collectResponsesToolGroups } from "./tool-groups";
8
9
 
9
10
  const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
@@ -65,6 +66,20 @@ export function routedCustomToolWireName(value: unknown): string | undefined {
65
66
  );
66
67
  }
67
68
 
69
+ /** Resolve a provider-emitted wire name to the routed custom tool the client declared. */
70
+ export function routedCustomToolTargetName(
71
+ value: unknown,
72
+ names: ReadonlySet<string>,
73
+ declaredNames?: ReadonlySet<string>,
74
+ ): string | undefined {
75
+ const wireName = routedCustomToolWireName(value);
76
+ if (wireName === undefined) return undefined;
77
+ if (names.has(wireName)) return wireName;
78
+ if (!isPlainObject(value) || typeof value.namespace === "string") return undefined;
79
+ const normalized = normalizeDeclaredToolName(wireName, declaredNames);
80
+ return normalized !== wireName && names.has(normalized) ? normalized : undefined;
81
+ }
82
+
68
83
  /**
69
84
  * Names of custom declarations after namespace lowering. The selection flag separates converted
70
85
  * names from native passthrough names while keeping same-named function and custom children distinct.
@@ -250,29 +265,37 @@ export function restoreRoutedCustomCalls(
250
265
  value: unknown,
251
266
  names: ReadonlySet<string>,
252
267
  repairNames: ReadonlySet<string> = new Set(),
268
+ declaredNames?: ReadonlySet<string>,
253
269
  ): { value: unknown; changed: boolean } {
254
270
  if (!isPlainObject(value)) return { value, changed: false };
255
271
 
256
272
  const restoreItem = (item: unknown): { value: unknown; changed: boolean } => {
257
273
  if (!isPlainObject(item)) return { value: item, changed: false };
258
274
  const wireName = routedCustomToolWireName(item);
275
+ const targetName = routedCustomToolTargetName(item, names, declaredNames);
259
276
  if (
260
- item.type === "function_call"
277
+ (item.type === "function_call" || item.type === "custom_tool_call")
261
278
  && typeof item.name === "string"
262
279
  && wireName !== undefined
263
- && names.has(wireName)
280
+ && targetName !== undefined
264
281
  ) {
282
+ const sourceInput = item.type === "function_call" ? item.arguments : item.input;
283
+ const aliased = targetName !== wireName;
265
284
  const restored: Record<string, unknown> = {
266
285
  ...item,
267
286
  type: "custom_tool_call",
268
287
  id: customToolItemId(item.id),
269
- input: repairFreeformToolInput(
270
- item.arguments,
271
- item.name,
272
- typeof item.namespace === "string" ? item.namespace : undefined,
273
- ),
288
+ name: aliased ? targetName : item.name,
289
+ input: aliased && sourceInput !== ""
290
+ ? compileCodeModeHelperInput(sourceInput, item.name)
291
+ : repairFreeformToolInput(
292
+ sourceInput,
293
+ targetName,
294
+ typeof item.namespace === "string" ? item.namespace : undefined,
295
+ ),
274
296
  };
275
297
  delete restored.arguments;
298
+ if (aliased) delete restored.namespace;
276
299
  return { value: restored, changed: true };
277
300
  }
278
301
  if (
@@ -323,7 +346,7 @@ export function restoreRoutedCustomCalls(
323
346
  && value.type.startsWith("response.")
324
347
  && isPlainObject(value.response)
325
348
  ) {
326
- const response = restoreRoutedCustomCalls(value.response, names, repairNames);
349
+ const response = restoreRoutedCustomCalls(value.response, names, repairNames, declaredNames);
327
350
  if (response.changed) {
328
351
  restored.response = response.value;
329
352
  changed = true;
@@ -337,6 +360,7 @@ export function restoreRoutedCustomCallsInJson(
337
360
  text: string,
338
361
  names: ReadonlySet<string>,
339
362
  repairNames: ReadonlySet<string> = new Set(),
363
+ declaredNames?: ReadonlySet<string>,
340
364
  ): string {
341
365
  if (names.size === 0 && repairNames.size === 0) return text;
342
366
  let payload: unknown;
@@ -345,7 +369,7 @@ export function restoreRoutedCustomCallsInJson(
345
369
  } catch {
346
370
  return text;
347
371
  }
348
- const restored = restoreRoutedCustomCalls(payload, names, repairNames);
372
+ const restored = restoreRoutedCustomCalls(payload, names, repairNames, declaredNames);
349
373
  return restored.changed ? JSON.stringify(restored.value) : text;
350
374
  }
351
375
 
@@ -45,6 +45,7 @@ type InputBlock =
45
45
  | { type: "input_text"; text: string }
46
46
  | { type: "text"; text: string }
47
47
  | { type: "input_image"; image_url?: string; file_id?: string; detail?: string }
48
+ | { type: "input_video"; video_url?: string }
48
49
  | { type: "input_file"; file_id?: string; filename?: string; file_data?: string };
49
50
 
50
51
  /** A usable reference string, or undefined. Empty strings and non-strings are not references. */
@@ -80,6 +81,9 @@ function inputContentParts(blocks: unknown): string | OcxContentPart[] {
80
81
  }
81
82
  // No usable reference: omit the block. A "[image: ?]" marker would claim an attachment
82
83
  // the request never carried, which is worse than dropping malformed input.
84
+ } else if (block.type === "input_video") {
85
+ const videoUrl = nonEmptyString(block.video_url);
86
+ if (videoUrl) parts.push({ type: "video", videoUrl });
83
87
  } else if (block.type === "input_file") {
84
88
  const b = block as { file_id?: string; filename?: string; file_data?: string };
85
89
  const fileId = nonEmptyString(b.file_id);
@@ -11,6 +11,10 @@ const inputImageBlockSchema = z.object({
11
11
  }).refine(v => typeof v.image_url === "string" || typeof v.file_id === "string", {
12
12
  message: "input_image requires at least one of image_url or file_id",
13
13
  });
14
+ const inputVideoBlockSchema = z.object({
15
+ type: z.literal("input_video"),
16
+ video_url: z.string().min(1),
17
+ });
14
18
  const inputFileBlockSchema = z.object({
15
19
  type: z.literal("input_file"),
16
20
  file_id: z.string().optional(),
@@ -24,7 +28,7 @@ const reasoningTextSchema = z.object({ type: z.literal("reasoning_text"), text:
24
28
  // codex-rs FunctionCallOutputContentItem (protocol/src/models.rs): input_text | input_image | encrypted_content.
25
29
  const encryptedContentBlockSchema = z.object({ type: z.literal("encrypted_content"), encrypted_content: z.string() });
26
30
 
27
- const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputFileBlockSchema]);
31
+ const inputContentBlockSchema = z.union([inputTextSchema, plainTextSchema, inputImageBlockSchema, inputVideoBlockSchema, inputFileBlockSchema]);
28
32
  const outputContentBlockSchema = z.union([outputTextSchema, plainTextSchema, outputRefusalSchema]);
29
33
  // Tool outputs on the wire mix codex-rs FunctionCallOutputContentItem with legacy output blocks.
30
34
  const toolOutputContentBlockSchema = z.union([
@@ -308,6 +308,23 @@ export function lookupReplayThoughtSignature(
308
308
  return entry.sig;
309
309
  }
310
310
 
311
+ /** Drop a remembered signature for a specific callId and scope (e.g. when upstream rejects it). */
312
+ export function forgetThoughtSignatureForReplay(
313
+ callId: string,
314
+ scope: OcxReasoningReplayScopeRef | undefined,
315
+ ): boolean {
316
+ const key = keyFor(callId, scope);
317
+ if (key === undefined) return false;
318
+ load();
319
+ const entry = entries.get(key);
320
+ if (!entry) return false;
321
+ entries.delete(key);
322
+ totalBytes -= entry.sig.length;
323
+ prune(Date.now());
324
+ void persist();
325
+ return true;
326
+ }
327
+
311
328
  /** Test seams: clear in-memory state and the loaded flag without touching the file. */
312
329
  export function resetThoughtSignatureReplayForTests(): void {
313
330
  entries = new Map();
package/src/router.ts CHANGED
@@ -33,6 +33,7 @@ import {
33
33
  OPENAI_CODEX_PROVIDER_ID,
34
34
  } from "./providers/openai-tiers";
35
35
  import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
36
+ import { resolveModelAlias } from "./providers/default-aliases";
36
37
  import { getStaleCached } from "./codex/model-cache";
37
38
  import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
38
39
  import {
@@ -117,6 +118,7 @@ export function knownModelIdsForProvider(
117
118
  registry?.modelReasoningEffortMap,
118
119
  registry?.modelMaxOutputTokens,
119
120
  registry?.modelSupportsServiceTier,
121
+ registry?.modelSupportsVerbosity,
120
122
  ]) {
121
123
  for (const id of Object.keys(map ?? {})) ids.add(id);
122
124
  }
@@ -318,6 +320,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
318
320
  : undefined,
319
321
  provider.modelSupportsServiceTier,
320
322
  );
323
+ const modelSupportsVerbosity = mergeRecordFill(
324
+ registryEntry.modelSupportsVerbosity,
325
+ provider.modelSupportsVerbosity,
326
+ );
321
327
  const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
322
328
  const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
323
329
  const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
@@ -435,6 +441,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
435
441
  ...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
436
442
  ...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
437
443
  ...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
444
+ ...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
438
445
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
439
446
  ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
440
447
  ...(reasoningEffortMap ? { reasoningEffortMap } : {}),
@@ -649,7 +656,14 @@ function routeModelInternal(
649
656
  // slash-containing model ids (e.g. "anthropic/claude-...") fall through when
650
657
  // no such provider exists.
651
658
  if (slash > 0) {
652
- const provName = modelId.slice(0, slash);
659
+ const requestedProvider = modelId.slice(0, slash);
660
+ const provName = hasOwnProvider(config.providers, requestedProvider)
661
+ ? requestedProvider
662
+ : Object.entries(config.providers).find(([, provider]) =>
663
+ typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0];
664
+ if (!provName) {
665
+ // A genuine slash-containing native model id still falls through unchanged.
666
+ } else {
653
667
  if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
654
668
  throw new Error(`No provider configured for model: ${modelId}`);
655
669
  }
@@ -665,14 +679,20 @@ function routeModelInternal(
665
679
  }
666
680
  // Codex-facing alias ids (`provider/vendor-model`) decode back to the native
667
681
  // slash id via an exact known-id lookup; raw full-slash selectors keep working.
682
+ const requestedModel = modelId.slice(slash + 1);
683
+ const decoded = decodeRoutedModelIdOrThrow(requestedModel, known);
684
+ const nativeModel = known.includes(decoded)
685
+ ? decoded
686
+ : resolveModelAlias(config, prov, known, requestedModel) ?? decoded;
668
687
  return routeResult(
669
688
  provName,
670
689
  prov,
671
- decodeRoutedModelIdOrThrow(modelId.slice(slash + 1), known),
690
+ nativeModel,
672
691
  "explicit-provider",
673
692
  "explicit-provider-namespace",
674
693
  );
675
694
  }
695
+ }
676
696
  }
677
697
 
678
698
  if (isBareOpenAiFamilyModel(modelId)) {
@@ -702,6 +722,24 @@ function routeModelInternal(
702
722
  }
703
723
  }
704
724
 
725
+ const aliasMatches: Array<{ provider: string; model: string; qualified: string }> = [];
726
+ for (const [provName, prov] of activeProviderEntries(config)) {
727
+ const known = knownModelIdsForProvider(provName, prov, config);
728
+ const native = resolveModelAlias(config, prov, known, modelId);
729
+ if (native) aliasMatches.push({
730
+ provider: provName,
731
+ model: native,
732
+ qualified: `${prov.alias || provName}/${modelId}`,
733
+ });
734
+ }
735
+ if (aliasMatches.length > 1) {
736
+ throw new Error(`model alias '${modelId}' is ambiguous: ${aliasMatches.map(match => match.qualified).sort().join(", ")}`);
737
+ }
738
+ if (aliasMatches[0]) {
739
+ const match = aliasMatches[0];
740
+ return routeResult(match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias");
741
+ }
742
+
705
743
  if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
706
744
  throw new Error(`No provider configured for model: ${modelId}`);
707
745
  }
@@ -119,6 +119,7 @@ const COOLDOWN_RECOVERY_KINDS = new Set([
119
119
  "key-429",
120
120
  "oauth-401",
121
121
  "anthropic-oauth-429",
122
+ "oauth-account-429",
122
123
  ]);
123
124
 
124
125
  function percentile(sorted: number[], p: number): number | undefined {
@@ -41,6 +41,12 @@ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuota
41
41
  const percents = [
42
42
  ...(monthly ? [] : [quota.weeklyPercent]),
43
43
  quota.monthlyPercent,
44
+ // The burst window is upstream-enforced independently of the governing window, so an
45
+ // account at 97% here has 3% headroom whatever its weekly figure says. This was invisible
46
+ // while the header parser misfiled 5h readings into weeklyPercent - routing saw the burst
47
+ // by accident. Once that is fixed, omitting it here reports 88% headroom for an account
48
+ // that is one request away from a 429. computeCodexUsageScore already folds it in.
49
+ quota.shortPercent,
44
50
  ].filter((value): value is number => typeof value === "number" && Number.isFinite(value));
45
51
  const maxPercent = percents.length > 0 ? Math.max(...percents) : undefined;
46
52
  // Credits-only snapshots prove neither usage nor exhaustion. Unknown must not
@@ -49,6 +55,10 @@ function codexAccountQuotaEvidence(accountId: string, plan?: string): RouteQuota
49
55
  const resets = [
50
56
  ...(monthly ? [] : [quota.weeklyResetAt]),
51
57
  quota.monthlyResetAt,
58
+ // Pair the reset with the window that can actually gate the next request: a burst-limited
59
+ // account recovers in hours, and reporting a distant weekly reset would defer a retry that
60
+ // is already safe.
61
+ quota.shortResetAt,
52
62
  ].filter((value): value is number => typeof value === "number" && Number.isFinite(value))
53
63
  .filter(value => value > Date.now());
54
64
  return {
@@ -13,6 +13,7 @@ import {
13
13
  azureCredentialConfigError,
14
14
  booleanRecordConfigError,
15
15
  modelAdapterRecordConfigError,
16
+ maxWsFrameBytesConfigError,
16
17
  nonBlankStringArrayConfigError,
17
18
  positiveIntegerConfigError,
18
19
  positiveIntegerRecordConfigError,
@@ -20,6 +21,7 @@ import {
20
21
  providerHeadersConfigError,
21
22
  reasoningSummaryDeliveryRecordConfigError,
22
23
  upstreamHttpVersionConfigError,
24
+ wsUpstreamConfigError,
23
25
  isAzureIdentityProvider,
24
26
  } from "../config/provider-validation";
25
27
  import { providerDestinationConfigError } from "../lib/destination-policy";
@@ -32,6 +34,7 @@ import { modelAutoCompactTokenLimitsConfigError } from "../providers/auto-compac
32
34
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
33
35
  import { xaiResponsesOptInState } from "../providers/xai-responses-opt-in";
34
36
  import { antigravityOAuthDestinationConfigError, getProviderTlsProfileStatus, providerTlsProfileConfigError } from "../lib/provider-tls-profile";
37
+ import { resolveAiStudioCredentials } from "../oauth/aistudio-credentials";
35
38
 
36
39
  let _corsOrigin = "http://localhost:10100";
37
40
  export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
@@ -571,6 +574,9 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
571
574
  delete canonicalCandidate.modelContextWindows;
572
575
  // User-owned soft compaction policy; it does not alter the canonical transport seed.
573
576
  delete canonicalCandidate.modelAutoCompactTokenLimits;
577
+ // Transport controls are user-owned overlays, not part of the immutable seed.
578
+ delete canonicalCandidate.wsUpstream;
579
+ delete canonicalCandidate.maxWsFrameBytes;
574
580
  const canonical = seed && sameCanonicalProviderSeed(canonicalCandidate, seed);
575
581
  if (!canonical) {
576
582
  return `provider ${name} must equal the canonical built-in provider seed`;
@@ -609,6 +615,10 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
609
615
  if (upstreamHttpVersionError) {
610
616
  return `provider ${JSON.stringify(redactSecretString(name))} ${upstreamHttpVersionError}`;
611
617
  }
618
+ const wsUpstreamError = wsUpstreamConfigError(raw.wsUpstream);
619
+ if (wsUpstreamError) return `provider ${name} ${wsUpstreamError}`;
620
+ const maxWsFrameBytesError = maxWsFrameBytesConfigError(raw.maxWsFrameBytes);
621
+ if (maxWsFrameBytesError) return `provider ${name} ${maxWsFrameBytesError}`;
612
622
  const modelCostsError = providerModelCostsConfigError(raw.modelCosts);
613
623
  if (modelCostsError) {
614
624
  // The provider name is caller-controlled and can be token-shaped; redact and JSON-escape
@@ -718,6 +728,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
718
728
  }
719
729
  for (const key of [
720
730
  "defaultModel",
731
+ "alias",
732
+ "modelAliases",
733
+ "defaultAliases",
721
734
  "disabled",
722
735
  "allowPrivateNetwork",
723
736
  "authMode",
@@ -732,6 +745,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
732
745
  "contextWindow",
733
746
  "modelContextWindows",
734
747
  "modelAutoCompactTokenLimits",
748
+ "wsUpstream",
749
+ "maxWsFrameBytes",
735
750
  "defaultMaxOutputTokens",
736
751
  "modelMaxOutputTokens",
737
752
  "openRouterRouting",
@@ -766,12 +781,20 @@ export function safeConfigDTO(config: OcxConfig): unknown {
766
781
  if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
767
782
  const codexAccountMode = providerCodexAccountMode(name, provider);
768
783
  if (codexAccountMode) dto.codexAccountMode = codexAccountMode;
784
+ if (effectiveGoogleMode(name, provider) === "ai-studio-web" || name === "google-aistudio") {
785
+ const credentials = resolveAiStudioCredentials(provider);
786
+ dto.hasAiStudioSession = credentials.kind === "ready";
787
+ dto.aiStudioAuthState = process.platform !== "darwin"
788
+ ? "unsupported"
789
+ : credentials.kind === "ready" ? "checking" : "needs_reauth";
790
+ }
769
791
  providers[name] = dto;
770
792
  }
771
793
  return {
772
794
  port: config.port,
773
795
  hostname: config.hostname ?? "127.0.0.1",
774
796
  defaultProvider: config.defaultProvider,
797
+ defaultModelAliases: config.defaultModelAliases,
775
798
  codexAutoStart: codexAutoStartEnabled(config),
776
799
  websockets: config.websockets,
777
800
  // The GUI's browser-open toggle reads and writes this; absent means the
@@ -19,7 +19,7 @@ import {
19
19
  responsesJsonToChatCompletion,
20
20
  responsesSseToChatCompletionsSse,
21
21
  } from "../chat/outbound";
22
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
22
+ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
23
23
  import { redactSecretString } from "../lib/redact";
24
24
  import { resolveClientRetryAfter } from "../lib/retry-after";
25
25
  import { estimateTokens } from "../lib/token-estimate";
@@ -121,6 +121,7 @@ async function handleChatCompletionsWithBudget(
121
121
  logCtx.model = route.modelId;
122
122
  logCtx.providerAdapter = route.provider.adapter;
123
123
  logCtx.requestedModel = requestedModel;
124
+ if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel;
124
125
  logCtx.provider = route.providerName;
125
126
  logCtx.routeDecision = route.routeDecision;
126
127
  settledRoute = route;
@@ -282,26 +283,26 @@ async function handleChatCompletionsWithBudget(
282
283
  const parsed = JSON.parse(text) as {
283
284
  error?: { message?: string; type?: string; code?: string | null } | string;
284
285
  message?: string;
286
+ type?: string;
287
+ code?: string | null;
285
288
  };
286
289
  const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined;
287
290
  const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
288
291
  const rawFallback = text
289
292
  ? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`
290
293
  : message;
291
- message = nested?.message || flat || rawFallback;
292
- if (nested) {
293
- if (typeof nested.type === "string") upstreamType = nested.type;
294
- if (nested.code === null || typeof nested.code === "string") upstreamCode = nested.code;
295
- }
294
+ const upstreamMessage = nested?.message || flat;
295
+ message = upstreamMessage
296
+ ? redactSecretString(upstreamMessage).slice(0, 500)
297
+ : rawFallback;
298
+ const structuredType = nested?.type ?? parsed.type;
299
+ const structuredCode = nested?.code ?? parsed.code;
300
+ if (typeof structuredType === "string") upstreamType = structuredType;
301
+ if (structuredCode === null || typeof structuredCode === "string") upstreamCode = structuredCode;
296
302
  } catch {
297
303
  if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
298
304
  }
299
305
  } catch { /* keep fallback */ }
300
- const retryAfter = resolveClientRetryAfter({
301
- status: upstream.status,
302
- message,
303
- upstreamRetryAfter: upstream.headers.get("retry-after"),
304
- });
305
306
  const classified = classifyError(
306
307
  upstream.status,
307
308
  upstreamType
@@ -311,9 +312,9 @@ async function handleChatCompletionsWithBudget(
311
312
  : "invalid_request_error"),
312
313
  message,
313
314
  );
314
- if (isCyberPolicyCode(upstreamCode)) {
315
+ if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
315
316
  classified.code = CYBER_POLICY_ERROR_CODE;
316
- classified.type = "invalid_request_error";
317
+ classified.type = cyberPolicyErrorType(upstreamType);
317
318
  } else if (upstreamCode === "model_not_found") {
318
319
  // Structured model_not_found must win over classifyError's generic remaps.
319
320
  classified.code = "model_not_found";
@@ -322,6 +323,13 @@ async function handleChatCompletionsWithBudget(
322
323
  classified.code = upstreamCode;
323
324
  }
324
325
  const status = isCyberPolicyCode(classified.code) ? 400 : upstream.status;
326
+ const retryAfter = isCyberPolicyCode(classified.code)
327
+ ? undefined
328
+ : resolveClientRetryAfter({
329
+ status: upstream.status,
330
+ message,
331
+ upstreamRetryAfter: upstream.headers.get("retry-after"),
332
+ });
325
333
  const rewritten = new Response(JSON.stringify({
326
334
  error: {
327
335
  message: classified.message,
@@ -388,14 +396,14 @@ async function handleChatCompletionsWithBudget(
388
396
  const status = (json as Rec)?.status;
389
397
  if (status === "failed") {
390
398
  const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error;
391
- const message = error?.message ?? "upstream request failed";
399
+ const message = redactSecretString(error?.message ?? "upstream request failed");
392
400
  const classified = classifyError(502, error?.type ?? "server_error", message);
393
401
  if (error?.code === "translation_buffer_limit") {
394
402
  classified.code = "translation_buffer_limit";
395
403
  classified.type = "upstream_error";
396
- } else if (isCyberPolicyCode(error?.code)) {
404
+ } else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
397
405
  classified.code = CYBER_POLICY_ERROR_CODE;
398
- classified.type = "invalid_request_error";
406
+ classified.type = cyberPolicyErrorType(error?.type);
399
407
  } else if (error?.code === "model_not_found") {
400
408
  // Same deliberate preserve as the non-OK path: structured code beats generic classify.
401
409
  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" };
@@ -116,6 +116,22 @@ export function stripEmptyLadderEffort(
116
116
  return Object.keys(next).length > 0 ? next : undefined;
117
117
  }
118
118
 
119
+ /**
120
+ * Strip reasoning effort from parsed options and _rawBody when the routed model
121
+ * has an empty supported ladder (effortless models like Composer 2.5).
122
+ */
123
+ export function sanitizeEffortForModel(
124
+ parsed: OcxParsedRequest,
125
+ ladder: readonly string[] | undefined,
126
+ ): void {
127
+ if (ladder === undefined || ladder.length > 0) return;
128
+ parsed.options.reasoning = undefined;
129
+ const raw = parsed._rawBody as { reasoning?: unknown } | undefined;
130
+ if (!raw || typeof raw !== "object" || !("reasoning" in raw)) return;
131
+ raw.reasoning = stripEmptyLadderEffort(raw.reasoning, ladder);
132
+ if (raw.reasoning === undefined) delete raw.reasoning;
133
+ }
134
+
119
135
  export function supportedLadderFor(route: { provider: OcxProviderConfig; modelId: string }): string[] | undefined {
120
136
  const { provider, modelId } = route;
121
137
  if (modelInList(provider.noReasoningModels, modelId)) return [];