@bitkyc08/opencodex 2.33.0 → 2.34.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 (159) hide show
  1. package/README.md +3 -3
  2. package/gui/dist/assets/index-C4TMRloX.js +112 -0
  3. package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  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 +162 -0
  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-desktop.ts +23 -0
  17. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  18. package/src/adapters/cursor/native-exec-network.ts +1 -1
  19. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  20. package/src/adapters/cursor/native-exec.ts +20 -0
  21. package/src/adapters/cursor/protobuf-request.ts +68 -14
  22. package/src/adapters/cursor/request-builder.ts +29 -6
  23. package/src/adapters/cursor/tool-definitions.ts +11 -2
  24. package/src/adapters/cursor/tool-result-normalize.ts +32 -1
  25. package/src/adapters/cursor/types.ts +20 -0
  26. package/src/adapters/cursor.ts +189 -37
  27. package/src/adapters/google-antigravity-replay.ts +102 -23
  28. package/src/adapters/google-errors.ts +44 -12
  29. package/src/adapters/google.ts +115 -16
  30. package/src/adapters/image.ts +1 -1
  31. package/src/adapters/kiro-constants.ts +15 -0
  32. package/src/adapters/kiro-tools.ts +43 -15
  33. package/src/adapters/kiro.ts +54 -9
  34. package/src/adapters/openai-chat.ts +286 -242
  35. package/src/adapters/openai-responses.ts +334 -24
  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 -24
  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 +32 -4
  44. package/src/cli/account-extended.ts +1 -1
  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/init.ts +1 -0
  51. package/src/cli/models-runtime.ts +95 -0
  52. package/src/cli/models.ts +13 -7
  53. package/src/cli/provider-runtime.ts +16 -2
  54. package/src/cli/registry.ts +6 -1
  55. package/src/cli/v2.ts +34 -10
  56. package/src/codex/account-pause.ts +2 -1
  57. package/src/codex/account-priority.ts +3 -2
  58. package/src/codex/app-server-processes.ts +80 -6
  59. package/src/codex/auth-api.ts +46 -8
  60. package/src/codex/auth-context.ts +21 -18
  61. package/src/codex/catalog/aggregation.ts +6 -0
  62. package/src/codex/catalog/native-models.ts +5 -2
  63. package/src/codex/catalog/parsing.ts +16 -0
  64. package/src/codex/catalog/provider-fetch.ts +20 -3
  65. package/src/codex/catalog/sync.ts +127 -2
  66. package/src/codex/catalog.ts +1 -1
  67. package/src/codex/codex-write-lock.ts +3 -1
  68. package/src/codex/convergence-types.ts +1 -1
  69. package/src/codex/convergence.ts +22 -2
  70. package/src/codex/desired-state.ts +2 -2
  71. package/src/codex/desktop-app-restart.ts +18 -5
  72. package/src/codex/inject-coordination.ts +83 -0
  73. package/src/codex/inject.ts +14 -1
  74. package/src/codex/log-guard/inspect.ts +22 -4
  75. package/src/codex/model-entitlements.ts +9 -2
  76. package/src/codex/prompt-layers.ts +371 -25
  77. package/src/codex/prompt-text-probe.ts +238 -0
  78. package/src/codex/quota.ts +91 -10
  79. package/src/codex/routing.ts +9 -0
  80. package/src/codex/subagent-model-fallback.ts +104 -25
  81. package/src/codex/transition-state.ts +107 -8
  82. package/src/combos/types.ts +10 -0
  83. package/src/compatibility/openai-responses.ts +33 -1
  84. package/src/config/rebase-provenance.ts +68 -0
  85. package/src/config.ts +107 -9
  86. package/src/generated/compatibility-version.json +191 -139
  87. package/src/images/loop.ts +5 -4
  88. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  89. package/src/lab/fabric/producer-child.ts +1 -1
  90. package/src/lib/config-ownership.ts +20 -0
  91. package/src/lib/errors.ts +11 -2
  92. package/src/lib/package-tree-integrity.ts +101 -0
  93. package/src/oauth/generic-account-failover.ts +231 -0
  94. package/src/oauth/index.ts +57 -5
  95. package/src/oauth/key-providers.ts +10 -1
  96. package/src/oauth/kiro.ts +45 -0
  97. package/src/oauth/types.ts +15 -0
  98. package/src/providers/command-code-efforts.ts +38 -6
  99. package/src/providers/context-cap.ts +4 -3
  100. package/src/providers/default-aliases.ts +65 -0
  101. package/src/providers/derive.ts +28 -0
  102. package/src/providers/fastwire.ts +7 -1
  103. package/src/providers/model-presets.ts +119 -0
  104. package/src/providers/new-model-policy.ts +146 -0
  105. package/src/providers/provider-id-rewrite.ts +2 -1
  106. package/src/providers/quota.ts +65 -7
  107. package/src/providers/registry.ts +160 -68
  108. package/src/providers/slug-codec.ts +52 -0
  109. package/src/responses/code-mode-helper-compat.ts +50 -0
  110. package/src/responses/custom-tool-compat.ts +34 -10
  111. package/src/responses/parser.ts +4 -0
  112. package/src/responses/schema.ts +5 -1
  113. package/src/responses/thought-signature-replay.ts +17 -0
  114. package/src/router.ts +40 -2
  115. package/src/routing/analytics.ts +1 -0
  116. package/src/routing/quota.ts +10 -0
  117. package/src/server/auth-cors.ts +4 -0
  118. package/src/server/chat-completions.ts +24 -16
  119. package/src/server/chat-native-sse.ts +3 -3
  120. package/src/server/chat-native.ts +30 -11
  121. package/src/server/claude-messages.ts +1 -1
  122. package/src/server/index.ts +77 -11
  123. package/src/server/lifecycle.ts +52 -1
  124. package/src/server/management/agent-settings-routes.ts +31 -15
  125. package/src/server/management/codex-prompt-routes.ts +570 -0
  126. package/src/server/management/combo-routes.ts +2 -1
  127. package/src/server/management/config-routes.ts +27 -9
  128. package/src/server/management/context.ts +9 -0
  129. package/src/server/management/model-routes.ts +266 -0
  130. package/src/server/management/provider-routes.ts +6 -0
  131. package/src/server/management/routing-profile-routes.ts +2 -2
  132. package/src/server/management-api.ts +2 -0
  133. package/src/server/relay-eager.ts +147 -20
  134. package/src/server/relay.ts +251 -19
  135. package/src/server/request-log-conversation.ts +21 -0
  136. package/src/server/request-log.ts +41 -15
  137. package/src/server/responses/collaboration.ts +37 -3
  138. package/src/server/responses/combo-stream-preflight.ts +10 -3
  139. package/src/server/responses/core.ts +494 -114
  140. package/src/server/responses/empty-completion-guard.ts +35 -0
  141. package/src/server/responses/fetch-helpers.ts +2 -1
  142. package/src/server/responses/input-admission.ts +3 -1
  143. package/src/server/responses/passthrough-error.ts +33 -9
  144. package/src/server/responses/responses-field-backfill.ts +105 -13
  145. package/src/server/responses-custom-tool-repair.ts +52 -7
  146. package/src/server/responses-terminal-repair.ts +25 -4
  147. package/src/server/sse-frame-buffer.ts +31 -4
  148. package/src/server/ws-bridge.ts +14 -2
  149. package/src/types/config.ts +53 -0
  150. package/src/types/provider.ts +64 -0
  151. package/src/types/request.ts +11 -3
  152. package/src/types/tools.ts +7 -5
  153. package/src/types.ts +1 -0
  154. package/src/usage/expected-prices.ts +43 -24
  155. package/src/usage/log.ts +3 -0
  156. package/src/web-search/exa-executor.ts +40 -9
  157. package/src/web-search/loop.ts +5 -4
  158. package/gui/dist/assets/index-23-Lf7jR.js +0 -102
  159. package/gui/dist/assets/index-DxJMDyOr.css +0 -1
@@ -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
@@ -32,6 +32,7 @@ import {
32
32
  OPENAI_CODEX_PROVIDER_ID,
33
33
  } from "./providers/openai-tiers";
34
34
  import { decodeRoutedModelIdOrThrow, encodeRoutedModelId } from "./providers/slug-codec";
35
+ import { resolveModelAlias } from "./providers/default-aliases";
35
36
  import { getStaleCached } from "./codex/model-cache";
36
37
  import { codexAccountNamespaceEntries } from "./codex/account-namespaces";
37
38
  import {
@@ -116,6 +117,7 @@ export function knownModelIdsForProvider(
116
117
  registry?.modelReasoningEffortMap,
117
118
  registry?.modelMaxOutputTokens,
118
119
  registry?.modelSupportsServiceTier,
120
+ registry?.modelSupportsVerbosity,
119
121
  ]) {
120
122
  for (const id of Object.keys(map ?? {})) ids.add(id);
121
123
  }
@@ -312,6 +314,10 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
312
314
  : undefined,
313
315
  provider.modelSupportsServiceTier,
314
316
  );
317
+ const modelSupportsVerbosity = mergeRecordFill(
318
+ registryEntry.modelSupportsVerbosity,
319
+ provider.modelSupportsVerbosity,
320
+ );
315
321
  const noVisionModels = mergeStringArray(registryEntry.noVisionModels, provider.noVisionModels);
316
322
  const noReasoningModels = mergeStringArray(registryEntry.noReasoningModels, provider.noReasoningModels);
317
323
  const noTemperatureModels = mergeStringArray(registryEntry.noTemperatureModels, provider.noTemperatureModels);
@@ -426,6 +432,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
426
432
  ...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
427
433
  ...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
428
434
  ...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
435
+ ...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
429
436
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
430
437
  ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
431
438
  ...(reasoningEffortMap ? { reasoningEffortMap } : {}),
@@ -640,7 +647,14 @@ function routeModelInternal(
640
647
  // slash-containing model ids (e.g. "anthropic/claude-...") fall through when
641
648
  // no such provider exists.
642
649
  if (slash > 0) {
643
- const provName = modelId.slice(0, slash);
650
+ const requestedProvider = modelId.slice(0, slash);
651
+ const provName = hasOwnProvider(config.providers, requestedProvider)
652
+ ? requestedProvider
653
+ : Object.entries(config.providers).find(([, provider]) =>
654
+ typeof provider.alias === "string" && provider.alias.toLowerCase() === requestedProvider.toLowerCase())?.[0];
655
+ if (!provName) {
656
+ // A genuine slash-containing native model id still falls through unchanged.
657
+ } else {
644
658
  if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
645
659
  throw new Error(`No provider configured for model: ${modelId}`);
646
660
  }
@@ -656,14 +670,20 @@ function routeModelInternal(
656
670
  }
657
671
  // Codex-facing alias ids (`provider/vendor-model`) decode back to the native
658
672
  // slash id via an exact known-id lookup; raw full-slash selectors keep working.
673
+ const requestedModel = modelId.slice(slash + 1);
674
+ const decoded = decodeRoutedModelIdOrThrow(requestedModel, known);
675
+ const nativeModel = known.includes(decoded)
676
+ ? decoded
677
+ : resolveModelAlias(config, prov, known, requestedModel) ?? decoded;
659
678
  return routeResult(
660
679
  provName,
661
680
  prov,
662
- decodeRoutedModelIdOrThrow(modelId.slice(slash + 1), known),
681
+ nativeModel,
663
682
  "explicit-provider",
664
683
  "explicit-provider-namespace",
665
684
  );
666
685
  }
686
+ }
667
687
  }
668
688
 
669
689
  if (isBareOpenAiFamilyModel(modelId)) {
@@ -693,6 +713,24 @@ function routeModelInternal(
693
713
  }
694
714
  }
695
715
 
716
+ const aliasMatches: Array<{ provider: string; model: string; qualified: string }> = [];
717
+ for (const [provName, prov] of activeProviderEntries(config)) {
718
+ const known = knownModelIdsForProvider(provName, prov, config);
719
+ const native = resolveModelAlias(config, prov, known, modelId);
720
+ if (native) aliasMatches.push({
721
+ provider: provName,
722
+ model: native,
723
+ qualified: `${prov.alias || provName}/${modelId}`,
724
+ });
725
+ }
726
+ if (aliasMatches.length > 1) {
727
+ throw new Error(`model alias '${modelId}' is ambiguous: ${aliasMatches.map(match => match.qualified).sort().join(", ")}`);
728
+ }
729
+ if (aliasMatches[0]) {
730
+ const match = aliasMatches[0];
731
+ return routeResult(match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias");
732
+ }
733
+
696
734
  if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
697
735
  throw new Error(`No provider configured for model: ${modelId}`);
698
736
  }
@@ -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 {
@@ -706,6 +706,9 @@ export function safeConfigDTO(config: OcxConfig): unknown {
706
706
  }
707
707
  for (const key of [
708
708
  "defaultModel",
709
+ "alias",
710
+ "modelAliases",
711
+ "defaultAliases",
709
712
  "disabled",
710
713
  "allowPrivateNetwork",
711
714
  "authMode",
@@ -757,6 +760,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
757
760
  port: config.port,
758
761
  hostname: config.hostname ?? "127.0.0.1",
759
762
  defaultProvider: config.defaultProvider,
763
+ defaultModelAliases: config.defaultModelAliases,
760
764
  codexAutoStart: codexAutoStartEnabled(config),
761
765
  websockets: config.websockets,
762
766
  // The GUI's browser-open toggle reads and writes this; absent means the
@@ -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" };