@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
@@ -239,6 +239,11 @@ export interface OcxClientIntegrationsConfig {
239
239
  "claude-desktop"?: boolean;
240
240
  }
241
241
 
242
+ export interface OcxConfigRebaseProvenance {
243
+ version: 1;
244
+ deletedTopLevelKeys: string[];
245
+ }
246
+
242
247
  export interface OcxConfig {
243
248
  port: number;
244
249
  /** Opt in to one identical-turn retry when a Responses completion has no text or tool call. */
@@ -260,6 +265,22 @@ export interface OcxConfig {
260
265
  managementUsageMaxReadBytes?: number;
261
266
  providers: Record<string, OcxProviderConfig>;
262
267
  defaultProvider: string;
268
+ /** Persisted state for newly discovered provider models (#2464). Absent keeps legacy "on" behavior. */
269
+ modelDiscovery?: {
270
+ newModelPolicy?: "on" | "off";
271
+ knownModels?: Record<string, {
272
+ ids: string[];
273
+ removed: string[];
274
+ updatedAt: string;
275
+ /** Consecutive successful discoveries in which an active id was absent. */
276
+ missing?: Record<string, number>;
277
+ }>;
278
+ recentArrivals?: Record<string, Array<{ id: string; at: string }>>;
279
+ };
280
+ /** Enable the shipped model alias patterns for providers without an override. */
281
+ defaultModelAliases?: boolean;
282
+ /** Explicit top-level deletion intent used by stale whole-config rebases. */
283
+ configRebaseProvenance?: OcxConfigRebaseProvenance | Record<string, unknown>;
263
284
  /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */
264
285
  openaiProviderTierVersion?: 1 | 2;
265
286
  /** One-time migration marker for Antigravity's static-catalog defaults. */
@@ -483,6 +504,13 @@ export interface OcxConfig {
483
504
  * those are unset — Bun's fetch honors them for all outbound calls; localhost is excluded.
484
505
  */
485
506
  proxy?: string;
507
+ /**
508
+ * Hosts that bypass `proxy` for OpenCodex's own outbound provider calls, merged into
509
+ * NO_PROXY at startup. Accepts a comma-separated string (NO_PROXY syntax) or an array.
510
+ * Loopback is always excluded regardless of this setting, and an inherited NO_PROXY is
511
+ * preserved — this ADDS entries, it never replaces the environment.
512
+ */
513
+ noProxy?: string | string[];
486
514
  /**
487
515
  * Upstream stall timeout (seconds). After this many seconds of no upstream data, emits
488
516
  * response.incomplete. Default 300. Min 1.
@@ -554,6 +582,15 @@ export interface OcxConfig {
554
582
  * selector map remains visible for compatibility with hand-written configurations.
555
583
  */
556
584
  codexAccountPickerEnabled?: boolean;
585
+ /**
586
+ * Show the GPT-5.3-Codex-Spark weekly window on Codex quota surfaces. Default false.
587
+ *
588
+ * Spark is a single-model window that reads 0% for most operators, and on a multi-account
589
+ * pool it doubles the bar count for information almost nobody acts on. Hidden by default and
590
+ * revealed by an explicit `true`; a malformed value reads as hidden rather than rejecting the
591
+ * whole config.
592
+ */
593
+ showCodexSparkQuota?: boolean;
557
594
  /** Active pool account id for next session. undefined = main (passthrough as-is). */
558
595
  activeCodexAccountId?: string;
559
596
  /** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
@@ -583,6 +620,22 @@ export interface OcxConfig {
583
620
  /** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
584
621
  stickyLimit?: number;
585
622
  };
623
+ /**
624
+ * Generic OAuth multi-account 429 failover (#2568). Presence-driven by default.
625
+ *
626
+ * Rotates to another logged-in account of the SAME provider when one is rate-limited, for
627
+ * OAuth providers that have no pool of their own — xAI, Cursor, Kimi, GitHub Copilot,
628
+ * Antigravity, Nous. The Codex pool and the Anthropic pool own their own rotation and are
629
+ * excluded; this setting changes neither.
630
+ *
631
+ * With the key absent, rotation activates when a provider has 2 or more eligible stored
632
+ * accounts — the same consent rule API-key pools already apply to a 2+ key pool (#2568d). A
633
+ * single account is a strict no-op. Set `false` to keep strict single-account behaviour;
634
+ * `providers.<name>.oauthAccountFailover` overrides this per provider.
635
+ */
636
+ oauthAccountFailover?: {
637
+ enabled?: boolean;
638
+ };
586
639
  /** Virtual `combo/<id>` models spanning concrete provider/model targets (issue #133). */
587
640
  combos?: Record<string, OcxComboConfig>;
588
641
  /**
@@ -115,6 +115,15 @@ export interface TierObservationContext {
115
115
  fastWire: FastWire | null;
116
116
  demandDecision: "force-fast" | "force-default" | "inherit";
117
117
  callerTier?: string;
118
+ /**
119
+ * Whether the destination's echoed `service_tier` is authoritative about Fast scheduling.
120
+ *
121
+ * The ChatGPT-internal Codex backend returns `service_tier: "default"` on turns that were in
122
+ * fact scheduled as priority, so treating its echo as a downgrade produced a false
123
+ * `response-declined` on every Fast request (#2558). Absent means "assume authoritative",
124
+ * preserving the behaviour for the public API where the echo does mean what it says.
125
+ */
126
+ responseTierAuthoritative?: boolean;
118
127
  }
119
128
 
120
129
  export type TierDecision =
@@ -127,6 +136,12 @@ export type TierDecision =
127
136
  * retries are allowed; OAuth/forward credentials and local runtimes are never replayed.
128
137
  */
129
138
  export interface OcxProviderConfig {
139
+ /** Optional short provider namespace used only at request/catalog presentation time. */
140
+ alias?: string;
141
+ /** Native model id -> short, slash-free request alias. */
142
+ modelAliases?: Record<string, string>;
143
+ /** Override the global built-in model-alias switch for this provider. */
144
+ defaultAliases?: boolean;
130
145
  adapter: string;
131
146
  /**
132
147
  * Codex tool calling mode for routed models.
@@ -266,6 +281,33 @@ export interface OcxProviderConfig {
266
281
  * full set so the user can pick). See devlog issue_052_provider-model-allowlist.
267
282
  */
268
283
  selectedModels?: string[];
284
+ /** Override for newly discovered models. Absent/"inherit" uses the install policy. */
285
+ newModelPolicy?: "on" | "off" | "inherit";
286
+ /**
287
+ * Model-preset marker for `selectedModels` (#2465). Absent means "all", exactly today's
288
+ * semantics — an existing provider is never narrowed by an upgrade.
289
+ *
290
+ * The preset is a SEED, not a lock: `selectedModels` holds concrete ids materialized from
291
+ * the shipped rules, so every existing consumer and older binaries keep working against a
292
+ * plain allowlist. Divergence is detected at the WRITE path rather than by diffing — any user
293
+ * edit while the mode is "preset" flips it to "custom", after which the proxy never
294
+ * re-materializes. That collapses upgrade reconciliation to a version compare.
295
+ *
296
+ * Deliberately distinct from `deriveProviderPresets`, which curates WHICH PROVIDERS to offer.
297
+ * This curates which MODELS a provider exposes; the code says "model preset" throughout.
298
+ */
299
+ modelPreset?: {
300
+ mode: "preset" | "all" | "custom";
301
+ /** MODEL_PRESETS version materialized into `selectedModels`. */
302
+ appliedVersion?: number;
303
+ appliedAt?: string;
304
+ /**
305
+ * Set when materialization matched nothing and the provider fell back to "all". A preset
306
+ * must never write an empty allowlist, because empty means ALL and would silently
307
+ * un-curate; the fallback marker lets the next convergence retry.
308
+ */
309
+ fallback?: "preset-empty";
310
+ };
269
311
  /** Provider-wide fallback when context metadata is absent; otherwise caps the reported window. */
270
312
  contextWindow?: number;
271
313
  /** Per-model fallback when context metadata is absent; otherwise caps the reported window. */
@@ -309,6 +351,16 @@ export interface OcxProviderConfig {
309
351
  * providers whose registry entry declares authKind "local" (management API enforces).
310
352
  */
311
353
  authMode?: "key" | "forward" | "oauth" | "local";
354
+ /**
355
+ * Per-provider override for generic OAuth multi-account 429 failover (#2568).
356
+ *
357
+ * Rotation is presence-driven by default — 2+ logged-in accounts activate it — so this exists
358
+ * for the operator who accepts rotation on one provider and refuses it on another. An explicit
359
+ * boolean here beats the global `oauthAccountFailover` and beats presence.
360
+ */
361
+ oauthAccountFailover?: {
362
+ enabled?: boolean;
363
+ };
312
364
  /** Allow an explicitly key/oauth provider to run without a credential (for keyless local proxies). */
313
365
  keyOptional?: boolean;
314
366
  /**
@@ -341,6 +393,18 @@ export interface OcxProviderConfig {
341
393
  * Responses backend rejects Codex summary-delivery fields for that model.
342
394
  */
343
395
  modelSupportsReasoningSummaries?: Record<string, boolean>;
396
+ /**
397
+ * Model-specific Codex Responses verbosity capability. Set false when the upstream ignores
398
+ * `text.verbosity`; the catalog hides the no-op picker and the Responses adapter strips stale
399
+ * or caller-supplied values while preserving other `text` fields.
400
+ */
401
+ modelSupportsVerbosity?: Record<string, boolean>;
402
+ /**
403
+ * Provider-wide Codex Responses verbosity capability, applied to models the per-model map
404
+ * does not enumerate (a live-discovered id, for example). Materialized from the registry at
405
+ * seed/enrich time so the catalog hint pass never has to read PROVIDER_REGISTRY.
406
+ */
407
+ supportsVerbosity?: boolean;
344
408
  /**
345
409
  * Per-model wire value for Responses `stream_options.reasoning_summary_delivery`.
346
410
  * Presence also advertises reasoning-summary support for that routed model.
@@ -68,6 +68,8 @@ export interface OcxParsedRequest {
68
68
  _cursorConversationId?: string;
69
69
  /** Stable upstream client thread identity, used only to derive provider-scoped continuation ids. */
70
70
  _clientThreadId?: string;
71
+ /** Cursor-only thread owner; may be an opaque process-local Desktop session/thread identity. */
72
+ _cursorClientThreadId?: string;
71
73
  /** Conversation/provider/account/model-bound namespace for reasoning replay state. */
72
74
  _reasoningReplayScope?: OcxReasoningReplayScopeRef;
73
75
  /**
@@ -86,7 +88,7 @@ export interface OcxParsedRequest {
86
88
  */
87
89
  _cursorIsolateConversation?: boolean;
88
90
  /** Account-scoped, non-secret Kiro request metadata selected with the OAuth access token. */
89
- _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
91
+ _kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion" | "authType">;
90
92
  /** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
91
93
  _providerContinuation?: OcxProviderContinuationState;
92
94
  /** Persisted continuation considered only after the final physical route is known. */
@@ -188,8 +190,14 @@ export interface OcxImageContent {
188
190
  detail?: string;
189
191
  }
190
192
 
191
- /** A user/developer message content part: text or an image (vision). */
192
- export type OcxContentPart = OcxTextContent | OcxImageContent;
193
+ export interface OcxVideoContent {
194
+ type: "video";
195
+ /** A base64 `data:` URL from an OpenAI-compatible `video_url` part. */
196
+ videoUrl: string;
197
+ }
198
+
199
+ /** A user/developer message content part: text or native media. */
200
+ export type OcxContentPart = OcxTextContent | OcxImageContent | OcxVideoContent;
193
201
 
194
202
  export interface OcxThinkingContent {
195
203
  type: "thinking";
@@ -32,16 +32,17 @@ 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).
43
43
  */
44
44
  const LEGACY_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const;
45
+ const CODE_MODE_HELPER_TOOL_NAMES = [...LEGACY_SHELL_BRIDGE_TOOL_NAMES, "apply_patch"] as const;
45
46
 
46
47
  export function normalizeDeclaredToolName(
47
48
  name: string,
@@ -49,13 +50,14 @@ export function normalizeDeclaredToolName(
49
50
  ): string {
50
51
  if (!declared || !declared.has("exec")) return name;
51
52
  if (declared.has(name)) return name;
53
+ if (name === "apply_patch") return "exec";
52
54
  // When the catalog explicitly declares any legacy shell bridge name, the environment
53
55
  // genuinely exposes that tool — turn normalization off so a call is never mis-routed
54
56
  // to `exec`.
55
57
  if ((LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).some(legacy => declared.has(legacy))) {
56
58
  return name;
57
59
  }
58
- return (LEGACY_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(name) ? "exec" : name;
60
+ return (CODE_MODE_HELPER_TOOL_NAMES as readonly string[]).includes(name) ? "exec" : name;
59
61
  }
60
62
 
61
63
  export function toolChoiceAliases(tool: Pick<OcxTool, "namespace" | "name">): string[] {
package/src/types.ts CHANGED
@@ -61,6 +61,7 @@ export type {
61
61
  OcxCustomModel,
62
62
  OcxApiKeyEntry,
63
63
  OcxClientIntegrationsConfig,
64
+ OcxConfigRebaseProvenance,
64
65
  OcxConfig,
65
66
  OcxAccountPoolRotationStrategy,
66
67
  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
@@ -126,6 +126,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [
126
126
  // Daybreak aliases: priced as their current snapshots (red -> gpt-5.6-cyber,
127
127
  // blue -> gpt-5.6-sol). The alias ids carry no rows of their own upstream, hence
128
128
  // verified-derived. Blue deliberately reuses GPT56_SOL rather than duplicating the tuple.
129
+ // The `gpt-` selector is native to ChatGPT/Codex accounts. The bare aliases below belong to
130
+ // the separately billed API-key catalog; keep those namespaces disjoint so pricing metadata
131
+ // cannot make an unroutable provider/model pair look supported.
132
+ { 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" },
129
133
  { 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" },
130
134
  { 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" },
131
135
  { 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" },
@@ -223,6 +227,8 @@ export function findExpectedPriceOverlay(
223
227
  /** OpenAI Fast price multipliers retained as a compatibility export. */
224
228
  export const PRIORITY_MULTIPLIERS: Readonly<Record<string, number>> = {
225
229
  "gpt-5.6-sol": 2,
230
+ "gpt-daybreak-blue-latest": 2,
231
+ "daybreak-blue-latest": 2,
226
232
  // Post-price-cut Fast tables (https://openai.com/api-fast-mode/, 2026-08-05):
227
233
  // Terra 4/24/0.40/5 and Luna 0.40/2.40/0.04/0.50 are both 2× the corrected
228
234
  // standard tuples; the stale bases made these look like 1.6/0.4 (#907).
@@ -257,13 +263,19 @@ const XAI_PRIORITY_PRICING = "https://docs.x.ai/developers/advanced-api-usage/pr
257
263
  */
258
264
  export const PRIORITY_PRICING_RULES: readonly PriorityPricingRule[] = [
259
265
  ...["openai", "openai-apikey"].flatMap(provider =>
260
- Object.entries(PRIORITY_MULTIPLIERS).map(([modelId, multiplier]): PriorityPricingRule => ({
261
- provider,
262
- modelId,
263
- multiplier,
264
- source: OPENAI_FAST_PRICING,
265
- verifiedAt: "2026-08-05",
266
- })),
266
+ Object.entries(PRIORITY_MULTIPLIERS)
267
+ // Daybreak's `gpt-` selector belongs to ChatGPT/Codex accounts; its bare selector belongs
268
+ // to the separately billed API-key catalog. All ordinary GPT ids remain valid on both.
269
+ .filter(([modelId]) => provider === "openai"
270
+ ? modelId !== "daybreak-blue-latest"
271
+ : modelId !== "gpt-daybreak-blue-latest")
272
+ .map(([modelId, multiplier]): PriorityPricingRule => ({
273
+ provider,
274
+ modelId,
275
+ multiplier,
276
+ source: OPENAI_FAST_PRICING,
277
+ verifiedAt: "2026-08-05",
278
+ })),
267
279
  ),
268
280
  ...["grok-4.5", "grok-4.6"].map((modelId): PriorityPricingRule => ({
269
281
  provider: "xai",
@@ -349,6 +361,29 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
349
361
  verifiedAt: "2026-08-03",
350
362
  })),
351
363
  ),
364
+ {
365
+ // The native Codex selector aliases gpt-5.6-sol and shares its 272k long-context tier.
366
+ provider: "openai",
367
+ modelId: "gpt-daybreak-blue-latest",
368
+ thresholdInputTokens: 272_000,
369
+ inclusive: false,
370
+ multiplier: OPENAI_LONG_CONTEXT,
371
+ confirmedPriorityRelation: "exclusive",
372
+ source: OPENAI_PRICING_DOC,
373
+ verifiedAt: "2026-08-11",
374
+ },
375
+ {
376
+ // The bare selector is the separately billed API-key alias. Daybreak Red has no tier row:
377
+ // the cyber snapshot's four long-context cells are all "-" on the pricing page.
378
+ provider: "openai-apikey",
379
+ modelId: "daybreak-blue-latest",
380
+ thresholdInputTokens: 272_000,
381
+ inclusive: false,
382
+ multiplier: OPENAI_LONG_CONTEXT,
383
+ confirmedPriorityRelation: "exclusive",
384
+ source: OPENAI_PRICING_DOC,
385
+ verifiedAt: "2026-08-11",
386
+ },
352
387
  {
353
388
  provider: "xai",
354
389
  modelId: "grok-4.5",
@@ -371,22 +406,6 @@ export const CONTEXT_TIERS: readonly ContextTier[] = [
371
406
  source: "https://docs.x.ai/developers/pricing",
372
407
  verifiedAt: "2026-08-18",
373
408
  },
374
- {
375
- // daybreak-blue-latest aliases gpt-5.6-sol, which publishes the full long-context row
376
- // ($10 / $1 / $12.50 / $45). Scoped to openai-apikey ON PURPOSE: Daybreak is not
377
- // routable on the Codex-login `openai` provider, so adding the alias to the shared
378
- // OPENAI_GPT56_CONTEXT_MODELS list (expanded across both providers above) would mint a
379
- // tier for a provider/model pair that cannot exist.
380
- // daybreak-red-latest has NO tier row: the cyber snapshot's four long-context cells are
381
- // all "-" on the pricing page (verified 2026-08-11).
382
- provider: "openai-apikey",
383
- modelId: "daybreak-blue-latest",
384
- thresholdInputTokens: 272_000,
385
- inclusive: false,
386
- multiplier: OPENAI_LONG_CONTEXT,
387
- source: OPENAI_PRICING_DOC,
388
- verifiedAt: "2026-08-11",
389
- },
390
409
  ...["minimax", "minimax-cn"].map((provider): ContextTier => ({
391
410
  provider,
392
411
  modelId: "MiniMax-M3",
package/src/usage/log.ts CHANGED
@@ -28,6 +28,7 @@ export type AttemptRecoveryKind =
28
28
  | "key-429"
29
29
  | "rate-limit-429"
30
30
  | "anthropic-oauth-429"
31
+ | "oauth-account-429"
31
32
  | "image-413"
32
33
  | "opaque-blob-rejection"
33
34
  | "empty-completion";
@@ -68,6 +69,7 @@ export interface PersistedUsageAttempt {
68
69
  }
69
70
 
70
71
  export interface PersistedUsageEntry {
72
+ requestedAlias?: string;
71
73
  requestId: string;
72
74
  timestamp: number;
73
75
  provider: string;
@@ -218,6 +220,7 @@ const ATTEMPT_RECOVERY_KINDS = new Set<AttemptRecoveryKind>([
218
220
  "key-429",
219
221
  "rate-limit-429",
220
222
  "anthropic-oauth-429",
223
+ "oauth-account-429",
221
224
  "image-413",
222
225
  "opaque-blob-rejection",
223
226
  "empty-completion",
@@ -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)`);
@@ -304,10 +304,11 @@ export interface WebSearchLoopDeps {
304
304
  /** Called before each routed-model dispatch in the loop, for attempt telemetry. Same-target 429 replays pass the `rate-limit-429` recovery kind. */
305
305
  onAttemptSend?: (recovery?: AttemptRecoveryKind) => void;
306
306
  /**
307
- * 429 key-failover hook: rotate the provider's active pool key and return a rebuilt adapter,
308
- * or null when the pool is exhausted (same semantics as the normal routed path).
307
+ * 429 failover hook: rotate the provider's active credential and return a rebuilt adapter,
308
+ * or null when the pool is exhausted. Async hooks support OAuth refresh; existing synchronous
309
+ * key-pool hooks remain valid.
309
310
  */
310
- on429?: (retryAfterHeader: string | null) => ProviderAdapter | null;
311
+ on429?: (retryAfterHeader: string | null) => ProviderAdapter | null | Promise<ProviderAdapter | null>;
311
312
  /** Opt-in same-target 429 policy (key-auth providers). When present, 429 replays on the SAME key before on429 rotation. */
312
313
  retryOn429Policy?: Required<RateLimitRetryPolicy> | null;
313
314
  /** Called only when the final bridged Responses stream reaches completed or incomplete. */
@@ -509,7 +510,7 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
509
510
  // 429 key-failover parity with the normal routed path: rotate pool keys until one responds
510
511
  // or the pool is exhausted (deps.on429 returns null — cooldown map guarantees termination).
511
512
  while (prepared.response.status === 429 && deps.on429) {
512
- const rotated = deps.on429(prepared.response.headers.get("retry-after"));
513
+ const rotated = await deps.on429(prepared.response.headers.get("retry-after"));
513
514
  if (!rotated) break;
514
515
  // Never let a broken body's cancel promise outlive the cumulative header deadline. Observe
515
516
  // it, but proceed immediately to the rotated fetch under the SAME deadline signal.