@yansigit/opencodex 2.33.0 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) 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 -1
  6. package/src/adapters/anthropic.ts +79 -2
  7. package/src/adapters/command-code.ts +141 -23
  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 +17 -1
  15. package/src/adapters/cursor/message-mapper.ts +4 -1
  16. package/src/adapters/cursor/native-exec-fs.ts +13 -12
  17. package/src/adapters/cursor/native-exec-network.ts +3 -5
  18. package/src/adapters/cursor/native-exec-policy.ts +47 -0
  19. package/src/adapters/cursor/native-exec-shell.ts +116 -31
  20. package/src/adapters/cursor/native-exec.ts +38 -10
  21. package/src/adapters/cursor/protobuf-events.ts +28 -2
  22. package/src/adapters/cursor/protobuf-request.ts +93 -41
  23. package/src/adapters/cursor/request-builder.ts +39 -10
  24. package/src/adapters/cursor/tool-definitions.ts +27 -3
  25. package/src/adapters/cursor/tool-result-normalize.ts +51 -6
  26. package/src/adapters/cursor/types.ts +23 -4
  27. package/src/adapters/cursor.ts +170 -29
  28. package/src/adapters/google-aistudio-parser.ts +49 -0
  29. package/src/adapters/google-antigravity-replay.ts +105 -25
  30. package/src/adapters/google-antigravity-wire.ts +5 -0
  31. package/src/adapters/google-errors.ts +41 -12
  32. package/src/adapters/google-http.ts +12 -11
  33. package/src/adapters/google.ts +219 -36
  34. package/src/adapters/image.ts +1 -1
  35. package/src/adapters/kiro-constants.ts +15 -0
  36. package/src/adapters/kiro-tools.ts +43 -15
  37. package/src/adapters/kiro.ts +54 -9
  38. package/src/adapters/openai-chat.ts +286 -242
  39. package/src/adapters/openai-responses.ts +335 -24
  40. package/src/adapters/run-turn-queue.ts +36 -1
  41. package/src/adapters/tool-catalog-nudge.ts +2 -2
  42. package/src/adapters/xai-tool-schema.ts +436 -0
  43. package/src/bridge.ts +67 -26
  44. package/src/chat/inbound.ts +29 -1
  45. package/src/chat/outbound.ts +15 -7
  46. package/src/claude/agents-inject.ts +8 -1
  47. package/src/claude/outbound.ts +10 -8
  48. package/src/cli/account-api.ts +27 -7
  49. package/src/cli/account-extended.ts +10 -3
  50. package/src/cli/account.ts +29 -5
  51. package/src/cli/alias.ts +66 -0
  52. package/src/cli/claude.ts +26 -1
  53. package/src/cli/dispatch.ts +13 -1
  54. package/src/cli/help.ts +1 -0
  55. package/src/cli/index.ts +6 -1
  56. package/src/cli/init.ts +1 -0
  57. package/src/cli/models-runtime.ts +95 -0
  58. package/src/cli/models.ts +13 -7
  59. package/src/cli/provider-runtime.ts +16 -2
  60. package/src/cli/registry.ts +6 -1
  61. package/src/cli/telemetry-commands.ts +25 -0
  62. package/src/cli/v2.ts +34 -10
  63. package/src/codex/account-pause.ts +2 -1
  64. package/src/codex/account-priority.ts +3 -2
  65. package/src/codex/app-server-processes.ts +80 -6
  66. package/src/codex/auth-api.ts +48 -8
  67. package/src/codex/auth-context.ts +21 -18
  68. package/src/codex/catalog/aggregation.ts +6 -0
  69. package/src/codex/catalog/model-metadata.ts +13 -1
  70. package/src/codex/catalog/native-models.ts +5 -2
  71. package/src/codex/catalog/parsing.ts +16 -0
  72. package/src/codex/catalog/provider-fetch.ts +20 -3
  73. package/src/codex/catalog/sync.ts +127 -2
  74. package/src/codex/catalog.ts +1 -1
  75. package/src/codex/codex-write-lock.ts +3 -1
  76. package/src/codex/convergence-types.ts +1 -1
  77. package/src/codex/convergence.ts +22 -2
  78. package/src/codex/desired-state.ts +2 -2
  79. package/src/codex/desktop-app-restart.ts +18 -5
  80. package/src/codex/inject-coordination.ts +83 -0
  81. package/src/codex/inject.ts +14 -1
  82. package/src/codex/log-guard/inspect.ts +22 -4
  83. package/src/codex/model-entitlements.ts +9 -2
  84. package/src/codex/prompt-layers.ts +371 -25
  85. package/src/codex/prompt-text-probe.ts +238 -0
  86. package/src/codex/quota.ts +123 -18
  87. package/src/codex/routing.ts +9 -0
  88. package/src/codex/subagent-model-fallback.ts +198 -27
  89. package/src/codex/transition-state.ts +107 -8
  90. package/src/combos/types.ts +10 -0
  91. package/src/compatibility/openai-responses.ts +33 -1
  92. package/src/config/autonomous-remediation.ts +21 -0
  93. package/src/config/provider-validation.ts +14 -0
  94. package/src/config/rebase-provenance.ts +68 -0
  95. package/src/config.ts +191 -17
  96. package/src/generated/compatibility-version.json +279 -159
  97. package/src/generated/model-metadata.ts +3 -0
  98. package/src/images/loop.ts +5 -4
  99. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  100. package/src/lab/fabric/producer-child.ts +1 -1
  101. package/src/lib/config-ownership.ts +20 -0
  102. package/src/lib/errors.ts +11 -2
  103. package/src/lib/package-tree-integrity.ts +101 -0
  104. package/src/oauth/aistudio-credentials.ts +65 -0
  105. package/src/oauth/aistudio-native-daemon.ts +116 -0
  106. package/src/oauth/aistudio-session-sync.ts +95 -0
  107. package/src/oauth/generic-account-failover.ts +231 -0
  108. package/src/oauth/google-aistudio-auth.ts +98 -0
  109. package/src/oauth/index.ts +57 -5
  110. package/src/oauth/key-providers.ts +18 -1
  111. package/src/oauth/kiro.ts +45 -0
  112. package/src/oauth/login-cli.ts +65 -1
  113. package/src/oauth/types.ts +15 -0
  114. package/src/providers/codex-capacity.ts +5 -2
  115. package/src/providers/command-code-efforts.ts +38 -6
  116. package/src/providers/context-cap.ts +4 -3
  117. package/src/providers/default-aliases.ts +65 -0
  118. package/src/providers/derive.ts +29 -1
  119. package/src/providers/fastwire.ts +7 -1
  120. package/src/providers/model-presets.ts +119 -0
  121. package/src/providers/new-model-policy.ts +146 -0
  122. package/src/providers/provider-id-rewrite.ts +2 -1
  123. package/src/providers/quota.ts +157 -46
  124. package/src/providers/registry.ts +184 -71
  125. package/src/providers/slug-codec.ts +52 -0
  126. package/src/responses/code-mode-helper-compat.ts +50 -0
  127. package/src/responses/custom-tool-compat.ts +34 -10
  128. package/src/responses/parser.ts +4 -0
  129. package/src/responses/schema.ts +5 -1
  130. package/src/responses/thought-signature-replay.ts +17 -0
  131. package/src/router.ts +43 -2
  132. package/src/routing/account-pool/cooldown.ts +8 -0
  133. package/src/routing/account-pool/index.ts +1 -0
  134. package/src/routing/analytics.ts +1 -0
  135. package/src/routing/quota.ts +10 -0
  136. package/src/server/auth-cors.ts +24 -0
  137. package/src/server/chat-completions.ts +26 -16
  138. package/src/server/chat-native-sse.ts +3 -3
  139. package/src/server/chat-native.ts +30 -11
  140. package/src/server/claude-messages.ts +1 -1
  141. package/src/server/effort-policy.ts +16 -0
  142. package/src/server/index.ts +180 -14
  143. package/src/server/lifecycle.ts +52 -1
  144. package/src/server/management/agent-settings-routes.ts +31 -15
  145. package/src/server/management/codex-prompt-routes.ts +570 -0
  146. package/src/server/management/combo-routes.ts +2 -1
  147. package/src/server/management/config-routes.ts +27 -9
  148. package/src/server/management/context.ts +9 -0
  149. package/src/server/management/logs-usage-routes.ts +11 -5
  150. package/src/server/management/model-routes.ts +266 -0
  151. package/src/server/management/oauth-account-routes.ts +13 -3
  152. package/src/server/management/provider-routes.ts +137 -3
  153. package/src/server/management/routing-profile-routes.ts +2 -2
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/port-reclaim.ts +19 -1
  156. package/src/server/relay-eager.ts +147 -20
  157. package/src/server/relay.ts +251 -19
  158. package/src/server/request-log-conversation.ts +33 -0
  159. package/src/server/request-log.ts +48 -21
  160. package/src/server/responses/collaboration.ts +42 -5
  161. package/src/server/responses/combo-stream-preflight.ts +10 -3
  162. package/src/server/responses/core.ts +575 -140
  163. package/src/server/responses/empty-completion-guard.ts +35 -0
  164. package/src/server/responses/fetch-helpers.ts +14 -6
  165. package/src/server/responses/input-admission.ts +3 -1
  166. package/src/server/responses/passthrough-error.ts +33 -9
  167. package/src/server/responses/policy-fallback.ts +1 -1
  168. package/src/server/responses/responses-field-backfill.ts +105 -13
  169. package/src/server/responses/ws-upstream.ts +35 -5
  170. package/src/server/responses-custom-tool-repair.ts +52 -7
  171. package/src/server/responses-terminal-repair.ts +25 -4
  172. package/src/server/sse-frame-buffer.ts +31 -4
  173. package/src/server/ws-bridge.ts +14 -2
  174. package/src/smoke/fingerprint-cache.ts +133 -0
  175. package/src/smoke/live-scenarios.ts +33 -0
  176. package/src/smoke/runner.ts +119 -0
  177. package/src/telemetry/dispatcher.ts +44 -0
  178. package/src/telemetry/fingerprint.ts +24 -0
  179. package/src/telemetry/hook.ts +43 -0
  180. package/src/telemetry/ledger.ts +54 -0
  181. package/src/telemetry/types.ts +23 -0
  182. package/src/types/config.ts +66 -14
  183. package/src/types/provider.ts +79 -1
  184. package/src/types/request.ts +18 -10
  185. package/src/types/tools.ts +30 -11
  186. package/src/types.ts +1 -0
  187. package/src/usage/command-code-manifest.ts +116 -0
  188. package/src/usage/cost.ts +2 -2
  189. package/src/usage/expected-prices.ts +126 -24
  190. package/src/usage/log.ts +18 -8
  191. package/src/usage/summary.ts +34 -12
  192. package/src/web-search/exa-executor.ts +40 -9
  193. package/src/web-search/index.ts +16 -8
  194. package/src/web-search/loop.ts +5 -4
  195. package/gui/dist/assets/index-DKLr4LTE.js +0 -102
  196. package/gui/dist/assets/index-DrSQdTRd.css +0 -1
@@ -101,3 +101,55 @@ export function slugEquivalenceKey(slug: string): string {
101
101
  export function slugsEquivalent(a: string, b: string): boolean {
102
102
  return a === b || slugEquivalenceKey(a) === slugEquivalenceKey(b);
103
103
  }
104
+
105
+ /**
106
+ * Resolve one config selection against a provider's known native ids (#2491).
107
+ *
108
+ * `slugEquivalenceKey` is deliberately lossy — the Codex one-slash rule forces `a/b` and
109
+ * `a-b` onto the same encoded form — so a selection written in either spelling matches BOTH
110
+ * when a provider publishes both. Filtering and persisted sync share that key, which keeps
111
+ * them consistent with each other but silently over-grants.
112
+ *
113
+ * This resolver keeps the tolerant behaviour (a selection still matches through either
114
+ * spelling, and an id absent from an incomplete live roster still resolves) while reporting
115
+ * whether the match was EXACT or merely equivalent. A caller that can afford to be strict —
116
+ * one holding a complete known-id set — can then prefer the exact row instead of granting the
117
+ * whole collision class.
118
+ *
119
+ * Returning the ambiguity rather than resolving it is deliberate: the roster is an incomplete
120
+ * dictionary, so silently narrowing to the exact spelling would hide a published id whenever
121
+ * discovery omitted it. The caller owns that tradeoff because only the caller knows whether
122
+ * its id set is complete.
123
+ */
124
+ export interface SlugSelectionMatch {
125
+ /** Native ids this selection admits. */
126
+ readonly matched: readonly string[];
127
+ /** The id whose raw form the selection names exactly, when one exists. */
128
+ readonly exact: string | undefined;
129
+ /** True when more than one known id shares the selection's encoded form. */
130
+ readonly ambiguous: boolean;
131
+ }
132
+
133
+ export function resolveSlugSelection(
134
+ provider: string,
135
+ selection: string,
136
+ knownIds: Iterable<string>,
137
+ ): SlugSelectionMatch {
138
+ // A slash in the selection is ambiguous on its own: `p/a-b` is provider-qualified, while
139
+ // `a/b` is a bare NATIVE id that happens to contain a slash. Treating every slash-bearing
140
+ // selection as provider-qualified made `a/b` resolve against provider "a", so the same
141
+ // collision reported ambiguous through the dash spelling and unambiguous through the slash
142
+ // spelling — the exact asymmetry this resolver exists to remove.
143
+ const qualified = selection.startsWith(`${provider}/`)
144
+ ? selection
145
+ : routedSlug(provider, selection);
146
+ const selectionKey = slugEquivalenceKey(qualified);
147
+ const matched: string[] = [];
148
+ let exact: string | undefined;
149
+ for (const id of knownIds) {
150
+ if (slugEquivalenceKey(routedSlug(provider, id)) !== selectionKey) continue;
151
+ matched.push(id);
152
+ if (id === selection || `${provider}/${id}` === selection) exact = id;
153
+ }
154
+ return { matched, exact, ambiguous: matched.length > 1 };
155
+ }
@@ -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);
@@ -398,6 +404,9 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
398
404
  authMode: canonicalAuthMode,
399
405
  apiKey: resolvedApiKey,
400
406
  ...(staticModelCatalog ? { liveModels: false } : {}),
407
+ ...(provider.requestPacing === undefined && registryEntry.requestPacing
408
+ ? { requestPacing: structuredClone(registryEntry.requestPacing) }
409
+ : {}),
401
410
  ...(headers ? { headers } : {}),
402
411
  // Backfill the Google wire mode + Vertex project/location from the registry when the user
403
412
  // config omits them, so a minimal `google-vertex`/`google-antigravity` entry still routes
@@ -432,6 +441,7 @@ export function routedProviderConfig(providerName: string, provider: OcxProvider
432
441
  ...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
433
442
  ...(modelMaxOutputTokens ? { modelMaxOutputTokens } : {}),
434
443
  ...(modelSupportsServiceTier ? { modelSupportsServiceTier } : {}),
444
+ ...(modelSupportsVerbosity ? { modelSupportsVerbosity } : {}),
435
445
  ...(modelReasoningEfforts ? { modelReasoningEfforts } : {}),
436
446
  ...(modelDefaultReasoningEfforts ? { modelDefaultReasoningEfforts } : {}),
437
447
  ...(reasoningEffortMap ? { reasoningEffortMap } : {}),
@@ -646,7 +656,14 @@ function routeModelInternal(
646
656
  // slash-containing model ids (e.g. "anthropic/claude-...") fall through when
647
657
  // no such provider exists.
648
658
  if (slash > 0) {
649
- 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 {
650
667
  if (provName === LEGACY_CHATGPT_PROVIDER_ID || provName === LEGACY_OPENAI_MULTI_PROVIDER_ID) {
651
668
  throw new Error(`No provider configured for model: ${modelId}`);
652
669
  }
@@ -662,14 +679,20 @@ function routeModelInternal(
662
679
  }
663
680
  // Codex-facing alias ids (`provider/vendor-model`) decode back to the native
664
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;
665
687
  return routeResult(
666
688
  provName,
667
689
  prov,
668
- decodeRoutedModelIdOrThrow(modelId.slice(slash + 1), known),
690
+ nativeModel,
669
691
  "explicit-provider",
670
692
  "explicit-provider-namespace",
671
693
  );
672
694
  }
695
+ }
673
696
  }
674
697
 
675
698
  if (isBareOpenAiFamilyModel(modelId)) {
@@ -699,6 +722,24 @@ function routeModelInternal(
699
722
  }
700
723
  }
701
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
+
702
743
  if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
703
744
  throw new Error(`No provider configured for model: ${modelId}`);
704
745
  }
@@ -63,6 +63,14 @@ export function clearCooldownState(poolKey?: string): void {
63
63
  registryByPool.delete(poolKey);
64
64
  }
65
65
 
66
+ export function clearPoolAccountCooldown(poolKey: string, accountId: string): boolean {
67
+ const registry = registryByPool.get(poolKey);
68
+ if (!registry) return false;
69
+ const existed = registry.get(accountId) !== null;
70
+ registry.clear(accountId);
71
+ return existed;
72
+ }
73
+
66
74
  export function parseRetryAfterMs(
67
75
  value: string | null | undefined,
68
76
  now = Date.now(),
@@ -26,6 +26,7 @@ export {
26
26
  STICK_WAIT_MAX_MS,
27
27
  classifyPoolHttpStatus,
28
28
  clearCooldownState,
29
+ clearPoolAccountCooldown,
29
30
  getPoolCooldownRegistry,
30
31
  isAccountInCooldown,
31
32
  isAccountPoolEligible,
@@ -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,9 +728,13 @@ 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",
737
+ "googleMode",
724
738
  "apiKeyTransport",
725
739
  "keyOptional",
726
740
  "freeTier",
@@ -731,6 +745,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
731
745
  "contextWindow",
732
746
  "modelContextWindows",
733
747
  "modelAutoCompactTokenLimits",
748
+ "wsUpstream",
749
+ "maxWsFrameBytes",
734
750
  "defaultMaxOutputTokens",
735
751
  "modelMaxOutputTokens",
736
752
  "openRouterRouting",
@@ -765,12 +781,20 @@ export function safeConfigDTO(config: OcxConfig): unknown {
765
781
  if (typeof registryNote === "string" && registryNote.trim()) dto.note = registryNote;
766
782
  const codexAccountMode = providerCodexAccountMode(name, provider);
767
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
+ }
768
791
  providers[name] = dto;
769
792
  }
770
793
  return {
771
794
  port: config.port,
772
795
  hostname: config.hostname ?? "127.0.0.1",
773
796
  defaultProvider: config.defaultProvider,
797
+ defaultModelAliases: config.defaultModelAliases,
774
798
  codexAutoStart: codexAutoStartEnabled(config),
775
799
  websockets: config.websockets,
776
800
  // The GUI's browser-open toggle reads and writes this; absent means the
@@ -9,6 +9,7 @@ import { FORWARD_HEADERS } from "../adapters/openai-responses";
9
9
  import {
10
10
  assertChatCompletionsRoutingBody,
11
11
  ChatCompletionsRequestError,
12
+ copyChatResponsesSessionHeaders,
12
13
  chatCompletionsToResponsesBody,
13
14
  } from "../chat/inbound";
14
15
  import {
@@ -18,7 +19,7 @@ import {
18
19
  responsesJsonToChatCompletion,
19
20
  responsesSseToChatCompletionsSse,
20
21
  } from "../chat/outbound";
21
- import { classifyError, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
22
+ import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors";
22
23
  import { redactSecretString } from "../lib/redact";
23
24
  import { resolveClientRetryAfter } from "../lib/retry-after";
24
25
  import { estimateTokens } from "../lib/token-estimate";
@@ -120,6 +121,7 @@ async function handleChatCompletionsWithBudget(
120
121
  logCtx.model = route.modelId;
121
122
  logCtx.providerAdapter = route.provider.adapter;
122
123
  logCtx.requestedModel = requestedModel;
124
+ if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel;
123
125
  logCtx.provider = route.providerName;
124
126
  logCtx.routeDecision = route.routeDecision;
125
127
  settledRoute = route;
@@ -201,6 +203,7 @@ async function handleChatCompletionsWithBudget(
201
203
  const value = req.headers.get(name);
202
204
  if (value) headers.set(name, value);
203
205
  }
206
+ copyChatResponsesSessionHeaders(req.headers, headers);
204
207
  // Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns.
205
208
  if (!directRoute) {
206
209
  // This enrichment is optional for routed/non-main providers. If native main
@@ -280,26 +283,26 @@ async function handleChatCompletionsWithBudget(
280
283
  const parsed = JSON.parse(text) as {
281
284
  error?: { message?: string; type?: string; code?: string | null } | string;
282
285
  message?: string;
286
+ type?: string;
287
+ code?: string | null;
283
288
  };
284
289
  const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined;
285
290
  const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message;
286
291
  const rawFallback = text
287
292
  ? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`
288
293
  : 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
- }
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;
294
302
  } catch {
295
303
  if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`;
296
304
  }
297
305
  } catch { /* keep fallback */ }
298
- const retryAfter = resolveClientRetryAfter({
299
- status: upstream.status,
300
- message,
301
- upstreamRetryAfter: upstream.headers.get("retry-after"),
302
- });
303
306
  const classified = classifyError(
304
307
  upstream.status,
305
308
  upstreamType
@@ -309,9 +312,9 @@ async function handleChatCompletionsWithBudget(
309
312
  : "invalid_request_error"),
310
313
  message,
311
314
  );
312
- if (isCyberPolicyCode(upstreamCode)) {
315
+ if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) {
313
316
  classified.code = CYBER_POLICY_ERROR_CODE;
314
- classified.type = "invalid_request_error";
317
+ classified.type = cyberPolicyErrorType(upstreamType);
315
318
  } else if (upstreamCode === "model_not_found") {
316
319
  // Structured model_not_found must win over classifyError's generic remaps.
317
320
  classified.code = "model_not_found";
@@ -320,6 +323,13 @@ async function handleChatCompletionsWithBudget(
320
323
  classified.code = upstreamCode;
321
324
  }
322
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
+ });
323
333
  const rewritten = new Response(JSON.stringify({
324
334
  error: {
325
335
  message: classified.message,
@@ -386,14 +396,14 @@ async function handleChatCompletionsWithBudget(
386
396
  const status = (json as Rec)?.status;
387
397
  if (status === "failed") {
388
398
  const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error;
389
- const message = error?.message ?? "upstream request failed";
399
+ const message = redactSecretString(error?.message ?? "upstream request failed");
390
400
  const classified = classifyError(502, error?.type ?? "server_error", message);
391
401
  if (error?.code === "translation_buffer_limit") {
392
402
  classified.code = "translation_buffer_limit";
393
403
  classified.type = "upstream_error";
394
- } else if (isCyberPolicyCode(error?.code)) {
404
+ } else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
395
405
  classified.code = CYBER_POLICY_ERROR_CODE;
396
- classified.type = "invalid_request_error";
406
+ classified.type = cyberPolicyErrorType(error?.type);
397
407
  } else if (error?.code === "model_not_found") {
398
408
  // Same deliberate preserve as the non-OK path: structured code beats generic classify.
399
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
  }