@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
@@ -32,16 +32,19 @@ export function namespacedToolName(namespace: string | undefined, name: string):
32
32
  }
33
33
 
34
34
  /**
35
- * Codex 0.149 unified-exec name normalization.
35
+ * Codex unified-exec name normalization.
36
36
  *
37
37
  * Codex's code-mode shell tool is declared as `exec` (a freeform custom tool whose own
38
38
  * description mentions the nested `await tools.exec_command(...)` helper). Routed models —
39
39
  * DeepSeek in particular — sometimes echo that helper name as the tool-call name, emitting
40
- * `exec_command` instead of the declared `exec`. Accept the legacy shell bridge names only
41
- * when the request catalog actually declares `exec` and does not itself declare the legacy
42
- * name (an MCP server may legitimately advertise `exec_command` under its own namespace).
40
+ * `exec_command` or `apply_patch` instead of the declared `exec`. Accept these nested helper
41
+ * names only when the request catalog actually declares `exec` and does not itself declare the
42
+ * emitted name (an MCP server may legitimately advertise one under its own namespace). Some
43
+ * hosted adapters also strip a unique namespace prefix or replace its separator with `_`; those
44
+ * forms are accepted only when one declaration can be identified unambiguously.
43
45
  */
44
46
  const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
47
+ const CODE_MODE_HELPER_TOOL_NAMES = [...LEGACY_SHELL_BRIDGE_TOOL_NAMES, "apply_patch"] as const;
45
48
 
46
49
  export function normalizeDeclaredToolName(
47
50
  name: string,
@@ -52,34 +55,26 @@ export function normalizeDeclaredToolName(
52
55
  if (declared.has("exec")) {
53
56
  // When the catalog explicitly declares any legacy shell bridge name, the environment
54
57
  // genuinely exposes that tool — turn normalization off so a call is never mis-routed
55
- // to exec.
58
+ // to `exec`.
56
59
  if ((LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).some(legacy => declared.has(legacy))) {
57
60
  return name;
58
61
  }
59
- if ((LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name)) {
62
+ if ((CODE_MODE_HELPER_TOOL_NAMES as readonly string[]).includes(name)) {
60
63
  return "exec";
61
64
  }
62
65
  }
63
- // When a model strips the namespace prefix (e.g. Gemini calling bare exec for default_api:exec),
64
- // or replaces non-alphanumeric separators with underscores, resolve it if and only if exactly
65
- // one declared tool matches.
66
- {
67
- let matched: string | undefined;
68
- for (const dec of declared) {
69
- const matchesSuffix = !name.includes(":") && !name.includes("__") && (dec.endsWith(":" + name) || dec.endsWith("__" + name));
70
- const matchesSanitized = dec.replace(/[^A-Za-z0-9_-]/g, "_") === name;
71
- if (matchesSuffix || matchesSanitized) {
72
- if (matched !== undefined) {
73
- return name;
74
- }
75
- matched = dec;
76
- }
77
- }
78
- if (matched !== undefined) {
79
- return matched;
80
- }
66
+ // Models may strip a namespace prefix or replace separators with underscores. Resolve only
67
+ // when exactly one declaration matches, so an ambiguous bare name remains fail-closed.
68
+ let matched: string | undefined;
69
+ for (const dec of declared) {
70
+ const matchesSuffix = !name.includes(":") && !name.includes("__")
71
+ && (dec.endsWith(`:${name}`) || dec.endsWith(`__${name}`));
72
+ const matchesSanitized = dec.replace(/[^A-Za-z0-9_-]/g, "_") === name;
73
+ if (!matchesSuffix && !matchesSanitized) continue;
74
+ if (matched !== undefined) return name;
75
+ matched = dec;
81
76
  }
82
- return name;
77
+ return matched ?? name;
83
78
  }
84
79
 
85
80
  export function toolChoiceAliases(tool: Pick<OcxTool, "namespace" | "name">): string[] {
package/src/types.ts CHANGED
@@ -66,6 +66,7 @@ export type {
66
66
  OcxApiKeyEntry,
67
67
  OcxClientIntegrationsConfig,
68
68
  OcxSubagentRole,
69
+ OcxConfigRebaseProvenance,
69
70
  OcxConfig,
70
71
  OcxAccountPoolRotationStrategy,
71
72
  OcxComboStrategy,
@@ -39,7 +39,7 @@ export interface ExpectedPriceOverlay {
39
39
  }
40
40
 
41
41
  const GEMINI_31_PRO: Cost4 = { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0 };
42
- const GPT56_SOL: Cost4 = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 };
42
+ const GPT56_SOL: Cost4 = { input: 4, output: 20, cacheRead: 0.4, cacheWrite: 5 };
43
43
  /**
44
44
  * Daybreak aliases. `daybreak-*-latest` never appears in the pricing table itself — only its
45
45
  * current snapshot does — so these tuples are the snapshot's published rates and carry
@@ -137,6 +137,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
137
137
  // Daybreak aliases: priced as their current snapshots (red -> gpt-5.6-cyber,
138
138
  // blue -> gpt-5.6-sol). The alias ids carry no rows of their own upstream, hence
139
139
  // verified-derived. Blue deliberately reuses GPT56_SOL rather than duplicating the tuple.
140
+ // The `gpt-` selector is native to ChatGPT/Codex accounts. The bare aliases below belong to
141
+ // the separately billed API-key catalog; keep those namespaces disjoint so pricing metadata
142
+ // cannot make an unroutable provider/model pair look supported.
143
+ { provider: "openai", modelId: "gpt-daybreak-blue-latest", cost4: GPT56_SOL, source: `alias of gpt-5.6-sol ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" },
140
144
  { provider: "openai-apikey", modelId: "daybreak-red-latest", cost4: DAYBREAK_RED, source: `alias of gpt-5.6-cyber ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" },
141
145
  { provider: "openai-apikey", modelId: "daybreak-blue-latest", cost4: GPT56_SOL, source: `alias of gpt-5.6-sol ${OPENAI_GPT56_PRICING}`, verifiedAt: "2026-08-11", status: "verified-derived" },
142
146
  { provider: "google-antigravity", modelId: "gemini-3.1-pro-low", cost4: GEMINI_31_PRO, source: `derived: gemini-3.1-pro (<=200k tier) ${GEMINI_PRICING}`, verifiedAt: "2026-07-20", status: "verified-derived" },
@@ -306,6 +310,8 @@ export function findExpectedPriceOverlay(
306
310
  /** OpenAI Fast price multipliers retained as a compatibility export. */
307
311
  export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
308
312
  "gpt-5.6-sol": 2,
313
+ "gpt-daybreak-blue-latest": 2,
314
+ "daybreak-blue-latest": 2,
309
315
  // Post-price-cut Fast tables (https://openai.com/api-fast-mode/, 2026-08-05):
310
316
  // Terra 4/24/0.40/5 and Luna 0.40/2.40/0.04/0.50 are both 2× the corrected
311
317
  // standard tuples; the stale bases made these look like 1.6/0.4 (#907).
@@ -340,13 +346,19 @@ const XAI_PRIORITY_PRICING = "https://docs.x.ai/developers/advanced-api-usage/pr
340
346
  */
341
347
  export const PRIORITY_PRICING_RULES: readonly PriorityPricingRule[] = [
342
348
  ...["openai", "openai-apikey"].flatMap(provider =>
343
- Object.entries(PRIORITY_MULTIPLIERS).map(([modelId, multiplier]): PriorityPricingRule => ({
344
- provider,
345
- modelId,
346
- multiplier,
347
- source: OPENAI_FAST_PRICING,
348
- verifiedAt: "2026-08-05",
349
- })),
349
+ Object.entries(PRIORITY_MULTIPLIERS)
350
+ // Daybreak's `gpt-` selector belongs to ChatGPT/Codex accounts; its bare selector belongs
351
+ // to the separately billed API-key catalog. All ordinary GPT ids remain valid on both.
352
+ .filter(([modelId]) => provider === "openai"
353
+ ? modelId !== "daybreak-blue-latest"
354
+ : modelId !== "gpt-daybreak-blue-latest")
355
+ .map(([modelId, multiplier]): PriorityPricingRule => ({
356
+ provider,
357
+ modelId,
358
+ multiplier,
359
+ source: OPENAI_FAST_PRICING,
360
+ verifiedAt: "2026-08-05",
361
+ })),
350
362
  ),
351
363
  ...["grok-4.5", "grok-4.6"].map((modelId): PriorityPricingRule => ({
352
364
  provider: "xai",
@@ -432,6 +444,29 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
432
444
  verifiedAt: "2026-08-03",
433
445
  })),
434
446
  ),
447
+ {
448
+ // The native Codex selector aliases gpt-5.6-sol and shares its 272k long-context tier.
449
+ provider: "openai",
450
+ modelId: "gpt-daybreak-blue-latest",
451
+ thresholdInputTokens: 272_000,
452
+ inclusive: false,
453
+ multiplier: OPENAI_LONG_CONTEXT,
454
+ confirmedPriorityRelation: "exclusive",
455
+ source: OPENAI_PRICING_DOC,
456
+ verifiedAt: "2026-08-11",
457
+ },
458
+ {
459
+ // The bare selector is the separately billed API-key alias. Daybreak Red has no tier row:
460
+ // the cyber snapshot's four long-context cells are all "-" on the pricing page.
461
+ provider: "openai-apikey",
462
+ modelId: "daybreak-blue-latest",
463
+ thresholdInputTokens: 272_000,
464
+ inclusive: false,
465
+ multiplier: OPENAI_LONG_CONTEXT,
466
+ confirmedPriorityRelation: "exclusive",
467
+ source: OPENAI_PRICING_DOC,
468
+ verifiedAt: "2026-08-11",
469
+ },
435
470
  {
436
471
  provider: "xai",
437
472
  modelId: "grok-4.5",
@@ -454,22 +489,6 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
454
489
  source: "https://docs.x.ai/developers/pricing",
455
490
  verifiedAt: "2026-08-18",
456
491
  },
457
- {
458
- // daybreak-blue-latest aliases gpt-5.6-sol, which publishes the full long-context row
459
- // ($10 / $1 / $12.50 / $45). Scoped to openai-apikey ON PURPOSE: Daybreak is not
460
- // routable on the Codex-login `openai` provider, so adding the alias to the shared
461
- // OPENAI_GPT56_CONTEXT_MODELS list (expanded across both providers above) would mint a
462
- // tier for a provider/model pair that cannot exist.
463
- // daybreak-red-latest has NO tier row: the cyber snapshot's four long-context cells are
464
- // all "-" on the pricing page (verified 2026-08-11).
465
- provider: "openai-apikey",
466
- modelId: "daybreak-blue-latest",
467
- thresholdInputTokens: 272_000,
468
- inclusive: false,
469
- multiplier: OPENAI_LONG_CONTEXT,
470
- source: OPENAI_PRICING_DOC,
471
- verifiedAt: "2026-08-11",
472
- },
473
492
  ...["minimax", "minimax-cn"].map((provider): ContextTier => ({
474
493
  provider,
475
494
  modelId: "MiniMax-M3",
package/src/usage/log.ts CHANGED
@@ -13,6 +13,15 @@ import { CODEX_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
13
13
  export type UsageStatus = "reported" | "unreported" | "unsupported" | "estimated";
14
14
  export type CodexUsageAccountLogLabel = "main" | `p${string}`;
15
15
 
16
+ export const OAUTH_ACCOUNT_LOG_LABEL_RE = /^[a-f0-9]{8,64}(?:-\d+)?$/;
17
+
18
+ export function isPersistableAccountLogLabel(value: unknown): value is string {
19
+ return (
20
+ value === "main"
21
+ || (typeof value === "string" && (CODEX_ACCOUNT_LOG_LABEL_RE.test(value) || OAUTH_ACCOUNT_LOG_LABEL_RE.test(value)))
22
+ );
23
+ }
24
+
16
25
  export function isCodexUsageAccountLogLabel(value: unknown): value is CodexUsageAccountLogLabel {
17
26
  return value === "main" || (typeof value === "string" && CODEX_ACCOUNT_LOG_LABEL_RE.test(value));
18
27
  }
@@ -30,6 +39,7 @@ export type AttemptRecoveryKind =
30
39
  | "anthropic-oauth-429"
31
40
  | "cursor-oauth-auth"
32
41
  | "cursor-oauth-429"
42
+ | "oauth-account-429"
33
43
  | "image-413"
34
44
  | "opaque-blob-rejection"
35
45
  | "empty-completion";
@@ -53,7 +63,7 @@ export interface PersistedUsageAttempt {
53
63
  recoveryKinds: AttemptRecoveryKind[];
54
64
  usageStatus: UsageStatus;
55
65
  /** Stable non-PII identity for the account that served this attempt. */
56
- accountLogLabel?: CodexUsageAccountLogLabel;
66
+ accountLogLabel?: string;
57
67
  inputTokenEstimate?: number;
58
68
  usage?: OcxUsage;
59
69
  totalTokens?: number;
@@ -70,6 +80,7 @@ export interface PersistedUsageAttempt {
70
80
  }
71
81
 
72
82
  export interface PersistedUsageEntry {
83
+ requestedAlias?: string;
73
84
  requestId: string;
74
85
  timestamp: number;
75
86
  provider: string;
@@ -81,8 +92,8 @@ export interface PersistedUsageEntry {
81
92
  admissionKind?: "configured" | "environment" | "loopback";
82
93
  /** The inbound wire, not the client product — see `surface`. */
83
94
  inboundProtocol?: "responses" | "chat" | "messages";
84
- /** Stable non-PII identity for account usage; absent for unauthenticated traffic. */
85
- accountLogLabel?: CodexUsageAccountLogLabel;
95
+ /** Stable non-PII identity for the account that served this request. */
96
+ accountLogLabel?: string;
86
97
  /** Best-effort chat/session correlation for Logs grouping (#330). */
87
98
  conversationId?: string;
88
99
  resolvedModel?: string;
@@ -220,8 +231,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
220
231
  "key-429",
221
232
  "rate-limit-429",
222
233
  "anthropic-oauth-429",
223
- "cursor-oauth-auth",
224
- "cursor-oauth-429",
234
+ "oauth-account-429",
225
235
  "image-413",
226
236
  "opaque-blob-rejection",
227
237
  "empty-completion",
@@ -367,7 +377,7 @@ function normalizeUsageAttempt(raw: unknown): PersistedUsageAttempt | null {
367
377
  sendCount: attempt.sendCount as number,
368
378
  recoveryKinds,
369
379
  usageStatus: attempt.usageStatus as UsageStatus,
370
- ...(isCodexUsageAccountLogLabel(attempt.accountLogLabel)
380
+ ...(isPersistableAccountLogLabel(attempt.accountLogLabel)
371
381
  ? { accountLogLabel: attempt.accountLogLabel }
372
382
  : {}),
373
383
  ...(isNonNegativeFiniteNumber(attempt.inputTokenEstimate)
@@ -454,7 +464,7 @@ function normalizeUsageEntry(entry: PersistedUsageEntry): PersistedUsageEntry {
454
464
  : {}),
455
465
  ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
456
466
  ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
457
- ...(isCodexUsageAccountLogLabel(entry.accountLogLabel)
467
+ ...(isPersistableAccountLogLabel(entry.accountLogLabel)
458
468
  ? { accountLogLabel: entry.accountLogLabel }
459
469
  : {}),
460
470
  ...(typeof entry.conversationId === "string" && entry.conversationId.trim()
@@ -9,10 +9,11 @@
9
9
  * Never throws; every error string passes redactSecretString.
10
10
  */
11
11
  import { fetchWithResetRetry } from "../lib/upstream-retry";
12
- import { signalWithTimeout } from "../lib/abort";
12
+ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
13
+ import { readBoundedResponseBytes } from "../lib/bounded-body";
13
14
  import { sidecarEnter } from "../lib/sidecar-tracker";
14
15
  import { redactSecretString } from "../lib/redact";
15
- import type { WebSearchSource } from "./parse";
16
+ import { MAX_SIDECAR_RESPONSE_BYTES, type WebSearchSource } from "./parse";
16
17
  import type { SidecarOutcome, SidecarSettings } from "./executor";
17
18
 
18
19
  const EXA_SEARCH_URL = "https://api.exa.ai/search";
@@ -47,14 +48,44 @@ export async function runExaWebSearch(
47
48
  }),
48
49
  { abortSignal: linkedSignal.signal, label: "exa-web-search-sidecar" },
49
50
  );
50
- if (!res.ok) {
51
- const t = await res.text().catch(() => "");
52
- // Scrub BEFORE truncating: slicing first can cut the literal key at the
53
- // boundary, leaving an unscrubbable key prefix in the surviving text.
54
- return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(t).slice(0, 200)}` };
51
+ const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
52
+ try {
53
+ let bounded: Awaited<ReturnType<typeof readBoundedResponseBytes>> | null = null;
54
+ try {
55
+ bounded = await readBoundedResponseBytes(res, {
56
+ maxBytes: MAX_SIDECAR_RESPONSE_BYTES,
57
+ signal: linkedSignal.signal,
58
+ });
59
+ } catch {
60
+ const reason = linkedSignal.signal.reason;
61
+ if (linkedSignal.signal.aborted && reason instanceof Error && reason.name === "TimeoutError") {
62
+ throw reason;
63
+ }
64
+ // Preserve the previous status-only/shapeless fallback for body read
65
+ // failures; fetch/header failures are still classified by the outer catch.
66
+ }
67
+ if (bounded?.oversized) {
68
+ const prefix = res.ok ? "exa sidecar response" : `exa sidecar HTTP ${res.status} response`;
69
+ return { text: "", sources: [], error: `${prefix} exceeded byte bound` };
70
+ }
71
+ // Response.text()/json() replace malformed UTF-8, so preserve that
72
+ // behavior while moving the byte bound ahead of decoding and parsing.
73
+ const text = bounded ? new TextDecoder().decode(bounded.bytes) : "";
74
+ if (!res.ok) {
75
+ // Scrub BEFORE truncating: slicing first can cut the literal key at the
76
+ // boundary, leaving an unscrubbable key prefix in the surviving text.
77
+ return { text: "", sources: [], error: `exa sidecar HTTP ${res.status}: ${scrub(text).slice(0, 200)}` };
78
+ }
79
+ let payload: unknown = null;
80
+ try {
81
+ payload = JSON.parse(text);
82
+ } catch {
83
+ // The mapper owns the stable malformed/empty JSON outcome.
84
+ }
85
+ return mapExaSearchResponse(payload);
86
+ } finally {
87
+ detachBodyGuard();
55
88
  }
56
- const payload = await res.json().catch(() => null);
57
- return mapExaSearchResponse(payload);
58
89
  } catch (e) {
59
90
  const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
60
91
  console.warn(`[web-search] exa sidecar ${kind} (${Date.now() - t0}ms)`);
@@ -318,10 +318,11 @@ export interface WebSearchLoopDeps {
318
318
  /** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
319
319
  onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
320
320
  /**
321
- * 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
322
- * or null when the pool is exhausted (same semantics as the normal routed path).
321
+ * 429 failover hook: rotate the provider's active credential and return a rebuilt adapter,
322
+ * or null when the pool is exhausted. Async hooks support OAuth refresh; existing synchronous
323
+ * key-pool hooks remain valid.
323
324
  */
324
- on429?: (retryAfterHeader: string | null) => ProviderAdapter | null;
325
+ on429?: (retryAfterHeader: string | null) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
325
326
  /** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
326
327
  retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
327
328
  /** Called only when the final bridged Responses stream reaches completed or incomplete. */
@@ -529,7 +530,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
529
530
  // 429 key-failover parity with the normal routed path: rotate pool keys until one responds
530
531
  // or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
531
532
  while (prepared.response.status === 429 && deps.on429) {
532
- const rotated = deps.on429(prepared.response.headers.get("retry-after"));
533
+ const rotated = await deps.on429(prepared.response.headers.get("retry-after"));
533
534
  if (!rotated) break;
534
535
  // Never let a broken body's cancel promise outlive the cumulative header deadline. Observe
535
536
  // it, but proceed immediately to the rotated fetch under the SAME deadline signal.