@bitkyc08/opencodex 2.83.0 → 2.84.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 (216) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
@@ -197,6 +197,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
197
197
  { method: "POST", path: "/api/codex-prompt/repair", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Prompt repair requires the gui-session principal (codex-prompt-routes.ts:298)." } },
198
198
  { method: "PUT", path: "/api/codex-prompt/base", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Base prompt write requires the gui-session principal (codex-prompt-routes.ts:298)." } },
199
199
  { method: "PUT", path: "/api/codex-prompt/base/select", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Base prompt selection requires the gui-session principal (codex-prompt-routes.ts:298)." } },
200
+ { method: "POST", path: "/api/codex-prompt/base/import", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Base prompt import requires the gui-session principal (codex-prompt-routes.ts:298)." } },
200
201
  { method: "PUT", path: "/api/codex-prompt/custom", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Custom prompt write requires the gui-session principal (codex-prompt-routes.ts:298)." } },
201
202
  { method: "PUT", path: "/api/codex-prompt/toggle", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Prompt toggle requires the gui-session principal (codex-prompt-routes.ts:298)." } },
202
203
  // server/management/combo-routes
@@ -390,11 +391,12 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
390
391
  // server/management/link-routes
391
392
  { method: "GET", path: "/api/link/status", module: "server/management/link-routes", mutates: false },
392
393
  { method: "GET", path: "/api/link/candidates", module: "server/management/link-routes", mutates: false, exempt: { reason: "session-only", why: "SSH candidates are a dashboard surface: a paired session, or on a standalone runtime the current loopback dashboard session on trusted loopback ingress; admin-token and Tailscale identity sessions are refused." } },
393
- { method: "POST", path: "/api/link/probe", module: "server/management/link-routes", mutates: true, exempt: { reason: "session-only", why: "SSH probing and host-key presentation are part of the interactive dashboard consent flow (paired, or standalone loopback dashboard on trusted loopback ingress)." } },
394
- { method: "POST", path: "/api/link/confirm-host", module: "server/management/link-routes", mutates: true, exempt: { reason: "session-only", why: "Persisting a host key requires the dashboard session that saw the fingerprint (paired, or standalone loopback dashboard on trusted loopback ingress)." } },
394
+ { method: "POST", path: "/api/link/probe", module: "server/management/link-routes", mutates: true },
395
+ { method: "POST", path: "/api/link/confirm-host", module: "server/management/link-routes", mutates: true },
395
396
  { method: "POST", path: "/api/link/join", module: "server/management/link-routes", mutates: true, exempt: { reason: "session-only", why: "Joining a confirmed Home issues a link credential and restarts this standalone runtime as a client on its configured port, which briefly interrupts running Codex turns. It requires an operator-paired dashboard session and a pre-join notice; unpaired standalone loopback, admin-token and Tailscale identity sessions are refused." } },
396
- { method: "POST", path: "/api/link/apply", module: "server/management/link-routes", mutates: true, exempt: { reason: "session-only", why: "Applying a link issues a data key and starts a remote tunnel, so it requires a dashboard session (paired, or standalone loopback dashboard on trusted loopback ingress)." } },
397
+ { method: "POST", path: "/api/link/apply", module: "server/management/link-routes", mutates: true },
397
398
  { method: "DELETE", path: "/api/link/{id}", module: "server/management/link-routes", mutates: true, mechanism: "regex" },
399
+ { method: "POST", path: "/api/link/{id}/reconnect", module: "server/management/link-routes", mutates: true, mechanism: "regex" },
398
400
  { method: "POST", path: "/api/link/issue", module: "server/management/link-routes", mutates: true },
399
401
  // server/management/remote-workspace-routes
400
402
  { method: "GET", path: "/api/remote-workspace", module: "server/management/remote-workspace-routes", mutates: false },
@@ -3,6 +3,7 @@ import { captureInitialSelectionBaseline, finalizeInitialModelSelection } from "
3
3
  import { captureModelDiscoveryBaseline, finalizeModelDiscovery } from "../../providers/new-model-policy-runtime";
4
4
  import { CatalogGatherBusyError } from "../../codex/catalog/routed-gather";
5
5
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
6
+ import { isPassiveRead } from "../../lib/passive-read";
6
7
  import { readFileSync } from "node:fs";
7
8
  import type { CatalogModel } from "../../codex/catalog";
8
9
  import { catalogModelSlug, invalidateCodexModelsCache, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
@@ -291,6 +292,8 @@ export async function fetchAllModels(
291
292
  /** Management renders disabled rows using this detached state after inventory drift. */
292
293
  publishProjection?: (projection: OcxConfig) => void,
293
294
  ): Promise<CatalogModel[]> {
295
+ // A Remote Link read: no upstream, no initial selection, no write (#6959).
296
+ if (isPassiveRead()) return [];
294
297
  const { gatherRoutedModels } = await import("../../codex/catalog");
295
298
  const baseline = captureInitialSelectionBaseline(config);
296
299
  const discoveryBaseline = captureModelDiscoveryBaseline(config);
@@ -54,7 +54,7 @@ import { noteExplicitShutdownRequested } from "./management/system-restart";
54
54
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "./request-log";
55
55
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../usage/cost";
56
56
  import type { PersistedUsageAttempt } from "../usage/log";
57
- import { isAllowedManagementOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "./auth-cors";
57
+ import { inheritTrustedHubManagementRequest, isAllowedManagementOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "./auth-cors";
58
58
  import { applySystemEnvToggle } from "./system-env";
59
59
 
60
60
  import type { ManagementApiDeps } from "./management/context";
@@ -242,7 +242,9 @@ export async function handleManagementAPI(
242
242
  if (verified.status !== 200) {
243
243
  return jsonResponse({ error: "account switch body rejected" }, verified.status, req, config);
244
244
  }
245
+ const sourceReq = req;
245
246
  req = new Request(req.url, { method: req.method, headers: req.headers, body: Buffer.from(verified.body) });
247
+ inheritTrustedHubManagementRequest(sourceReq, req);
246
248
  }
247
249
  // Management bodies are small JSON (provider names, key ids, settings). Reject oversized
248
250
  // payloads before any handler buffers them — the data plane has its own decompression cap.
@@ -115,6 +115,8 @@ import {
115
115
  type RequestLogContext,
116
116
  } from "./request-log";
117
117
  import { fetchWithHeaderTimeout, providerFetch, safeHostLabel, sendWithConnectionPolicy } from "./responses/fetch-helpers";
118
+ import { anthropicBillingPhysicalFetch } from "../providers/anthropic-billing-ownership";
119
+ import { rotateAnthropicBillingKeyOnResponse } from "../providers/anthropic-billing-recovery";
118
120
  import { linkAbortSignal } from "./responses/core-lifetime";
119
121
  import { attachRequestSpendTracker } from "./responses/request-spend";
120
122
  import { workflowRefusalResponse } from "./workflow-refusal";
@@ -441,6 +443,8 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
441
443
  if (oauthBinding) logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
442
444
  const spendTracker = attachRequestSpendTracker(req, logCtx);
443
445
  const physicalBudget = createInferenceSendBudget(req, logCtx);
446
+ // Observe-only spend is already booked by the tracker; count billing sends without booking again.
447
+ let physicalSendsUsed = 0;
444
448
  let activeRequest: AnthropicMessagesPassthroughRequest;
445
449
  let retainedRequestBytes = 0;
446
450
  const releaseRetainedRequest = () => {
@@ -589,8 +593,9 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
589
593
  // exact incarnation across its await; a returned response can never reserve one.
590
594
  sendOwner = snapshot ? captureAnthropicPhysicalSendOwnership(snapshot) : null;
591
595
  if (snapshot && !sendOwner) throw new NativeOAuthSelectionChangedError();
596
+ physicalSendsUsed++;
592
597
  dispatched = await sendWithConnectionPolicy(
593
- (activeProvider as OcxProviderTransport).fetch ?? execute,
598
+ anthropicBillingPhysicalFetch((activeProvider as OcxProviderTransport).fetch ?? execute, route.providerName, activeProvider),
594
599
  wire.url,
595
600
  applyUpstreamRecoveryInit({ ...init, method: "POST", headers, body: wire.body }, transportRecovery),
596
601
  { providerName: route.providerName, provider: activeProvider },
@@ -638,49 +643,70 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
638
643
  let response: Response;
639
644
  try {
640
645
  response = await send();
641
- // A credential-scoped 401 on a static key pool says nothing about the sibling keys.
642
- while (response.status === 401 && hasKeyPoolFailover(activeProvider) && transientSendAvailable()) {
643
- const rotated = rotateProviderTransportOn401(config, route.providerName, activeProvider, {
644
- now: Date.now(),
645
- attemptedKey: activeProvider.apiKey,
646
- });
647
- if (!rotated) break;
648
- discard(response);
649
- rebuildFor(rotated);
650
- response = await send("key-401");
651
- }
652
646
  const retryPolicy = oauthBinding ? undefined : rateLimitRetryPolicyFor(activeProvider);
653
647
  let retries = 0;
654
- // A refusal this proxy synthesized for a reset replay is not a provider rate limit.
655
- while (
656
- response.status === 429
657
- && !isNonReplayableResponse(response)
658
- && retryPolicy
659
- && retries < retryPolicy.attempts
660
- && transientSendAvailable()
661
- ) {
662
- retries += 1;
663
- for await (const _ of prepareSameTarget429Wait({
664
- body: response.body,
665
- signal: upstream.signal,
666
- delayMs: rateLimitRetryDelayMs(retryPolicy, response.headers.get("retry-after"), Date.now()),
667
- })) { /* pre-stream wait */ }
668
- if (upstream.signal.aborted) throw upstream.signal.reason;
669
- response = await send("rate-limit-429");
670
- }
671
- while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
672
- const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
673
- retryAfter: response.headers.get("retry-after"),
674
- now: Date.now(),
675
- attemptedKey: activeProvider.apiKey,
676
- attemptedSelection: activeProvider._apiKeyAttempt,
677
- });
678
- if (!rotated) break;
679
- // Rotation already recorded the cooldown; keep the terminal 429 when no send remains.
680
- if (!transientSendAvailable()) break;
681
- discard(response);
682
- rebuildFor(rotated);
683
- response = await send("key-429");
648
+ let billingRecoveryActive = false;
649
+ const keySendAvailable = () => transientSendAvailable()
650
+ && (!billingRecoveryActive || Math.max(physicalSendsUsed, physicalBudget.used) < physicalBudget.policy.maxTotalModelSends);
651
+ for (;;) {
652
+ const priorResponse = response;
653
+ // A credential-scoped 401 on a static key pool says nothing about the sibling keys.
654
+ while (response.status === 401 && hasKeyPoolFailover(activeProvider) && keySendAvailable()) {
655
+ const rotated = rotateProviderTransportOn401(config, route.providerName, activeProvider, {
656
+ now: Date.now(),
657
+ attemptedKey: activeProvider.apiKey,
658
+ });
659
+ if (!rotated) break;
660
+ discard(response);
661
+ rebuildFor(rotated);
662
+ response = await send("key-401");
663
+ }
664
+ while ((response.status === 400 || response.status === 402) && hasKeyPoolFailover(activeProvider)) {
665
+ const rotated = await rotateAnthropicBillingKeyOnResponse(response, config, route.providerName, activeProvider, { signal: upstream.signal });
666
+ if (!rotated) break;
667
+ billingRecoveryActive = true;
668
+ if (!keySendAvailable()) break;
669
+ discard(response);
670
+ rebuildFor(rotated);
671
+ response = await send("key-401");
672
+ }
673
+ // A refusal this proxy synthesized for a reset replay is not a provider rate limit.
674
+ while (
675
+ response.status === 429
676
+ && !isNonReplayableResponse(response)
677
+ && retryPolicy
678
+ && retries < retryPolicy.attempts
679
+ && keySendAvailable()
680
+ ) {
681
+ retries += 1;
682
+ for await (const _ of prepareSameTarget429Wait({
683
+ body: response.body,
684
+ signal: upstream.signal,
685
+ delayMs: rateLimitRetryDelayMs(retryPolicy, response.headers.get("retry-after"), Date.now()),
686
+ })) { /* pre-stream wait */ }
687
+ if (upstream.signal.aborted) throw upstream.signal.reason;
688
+ response = await send("rate-limit-429");
689
+ }
690
+ while (response.status === 429 && !isNonReplayableResponse(response) && hasKeyPoolFailover(activeProvider)) {
691
+ const rotated = rotateProviderTransportOn429(config, route.providerName, activeProvider, {
692
+ retryAfter: response.headers.get("retry-after"),
693
+ now: Date.now(),
694
+ attemptedKey: activeProvider.apiKey,
695
+ attemptedSelection: activeProvider._apiKeyAttempt,
696
+ });
697
+ if (!rotated) break;
698
+ // Rotation already recorded the cooldown; keep the terminal 429 when no send remains.
699
+ if (!keySendAvailable()) break;
700
+ discard(response);
701
+ rebuildFor(rotated);
702
+ response = await send("key-429");
703
+ }
704
+ // A sibling's next refusal may change class; the same counters survive that transition.
705
+ if (response !== priorResponse && (
706
+ (billingRecoveryActive && [401, 429].includes(response.status))
707
+ || (activeProvider.adapter === "anthropic" && hasKeyPoolFailover(activeProvider) && [400, 402].includes(response.status))
708
+ )) continue;
709
+ break;
684
710
  }
685
711
  const oauthRetryKey = {};
686
712
  const triedAccountIds = new Set<string>();
@@ -480,6 +480,8 @@ export function relaySseEagerBounded(
480
480
  }
481
481
  }
482
482
  } finally {
483
+ upstream.signal.removeEventListener("abort", wakeUp);
484
+ upstream.signal.removeEventListener("abort", wakeParkedRead);
483
485
  // Release any retained rewrite-buffer bytes on every teardown path
484
486
  // (error, cancel, upstream abort) — consumption/EOF release alone
485
487
  // leaves them charged.
@@ -52,7 +52,7 @@ export function queryRequestLogs(
52
52
  for (const entry of logs) {
53
53
  if (provider && entry.provider !== provider && !entry.attempts?.some(attempt => attempt.provider === provider)) continue;
54
54
  if (conversationId && !matchesLogConversationId(entry.conversationId, conversationId)) continue;
55
- if (model && entry.model !== model && !entry.attempts?.some(attempt => attempt.model === model)) continue;
55
+ if (model && !matchesModel(entry, model) && !entry.attempts?.some(attempt => matchesModel(attempt, model))) continue;
56
56
  if (account && entry.accountLogLabel !== account && !entry.attempts?.some(attempt => attempt.accountLogLabel === account)) continue;
57
57
  if (protocolMode && !matchesProtocolMode(entry, protocolMode)) continue;
58
58
  if (status && (statusClass === undefined ? String(entry.status) !== status : Math.floor(entry.status / 100) !== statusClass)) continue;
@@ -91,3 +91,8 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
91
91
  export function filteredRequestLogCount(logs: RequestLogEntry[], params: URLSearchParams): number {
92
92
  return queryRequestLogs(logs, params).total;
93
93
  }
94
+
95
+ /** A bare ID may itself contain slashes; compare against each observed destination as a whole. */
96
+ function matchesModel(row: { provider?: string; model?: string }, model: string): boolean {
97
+ return row.model === model || (row.provider !== undefined && row.model !== undefined && `${row.provider}/${row.model}` === model);
98
+ }
@@ -7,6 +7,9 @@ import {
7
7
  recognizedUpstreamError,
8
8
  } from "../lib/errors";
9
9
  import { redactSecretString } from "../lib/redact";
10
+ import { outboundCredentials, type OutboundCredentialRegistry } from "../lib/outbound-credential-registry";
11
+ import { configuredCredentials, type ConfiguredCredentials } from "./configured-credentials";
12
+ import { codexWsWrapperStatus } from "./responses/codex-ws-wire";
10
13
 
11
14
  /** First bounded upstream diagnostics shared by request contexts and final rows. */
12
15
  export interface UpstreamErrorDiagnostics {
@@ -24,6 +27,9 @@ export interface UpstreamErrorPayload {
24
27
  type?: unknown;
25
28
  code?: unknown;
26
29
  message?: unknown;
30
+ /** Codex WebSocket wrapper status (`status_code` is its alias); read only on bare `error` events. */
31
+ status?: unknown;
32
+ status_code?: unknown;
27
33
  error?: { type?: unknown; code?: unknown; message?: unknown };
28
34
  last_error?: { type?: unknown; code?: unknown; message?: unknown };
29
35
  response?: {
@@ -74,9 +80,29 @@ function noteBoundedDiagnostic(
74
80
  if (logCtx[field] !== undefined) return;
75
81
  if (!isSafeDiagnosticToken(value)) return;
76
82
  if (field === "upstreamErrorType" && !KNOWN_UPSTREAM_ERROR_TYPES.has(value)) return;
83
+ if (!diagnosticValueAllowed(value)) return;
77
84
  logCtx[field] = value;
78
85
  const status = logCtx.terminalHttpStatus;
79
- if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
86
+ if (status === undefined || status >= 500) {
87
+ const { upstreamErrorCode: code, upstreamRequestId: requestId } = upstreamDiagnosticLogFields(logCtx);
88
+ console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${code ? ` code=${code}` : ""}${requestId ? ` request_id=${requestId}` : ""}`);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * A diagnostic must not repeat a configured credential or a credential this process sent upstream
94
+ * (an echo). All three fields fail closed while either source cannot vouch for complete coverage:
95
+ * a closed type vocabulary does not prove a member is not also a credential. Applied at capture,
96
+ * again for every retained value interpolated into a warning, and at projection, because a
97
+ * credential can be configured or sent after the value was captured.
98
+ */
99
+ export function diagnosticValueAllowed(
100
+ value: string,
101
+ registry: OutboundCredentialRegistry = outboundCredentials(),
102
+ configured: ConfiguredCredentials = configuredCredentials(),
103
+ ): boolean {
104
+ if (!registry.complete() || !configured.complete) return false;
105
+ return !registry.matches(value) && !configured.matches(value);
80
106
  }
81
107
 
82
108
  export function noteUpstreamRequestId(logCtx: TerminalStatusContext, headers: Headers): void {
@@ -94,13 +120,37 @@ export function captureUpstreamTerminalDiagnostics(logCtx: TerminalStatusContext
94
120
  }
95
121
 
96
122
  export function upstreamDiagnosticLogFields(logCtx: UpstreamErrorDiagnostics): UpstreamErrorDiagnostics {
123
+ // One verification per projection: the three fields are checked at the same instant, so they
124
+ // share one snapshot of each source, taken only once a field is actually populated (a row with
125
+ // no upstream diagnostic reads no credential store).
126
+ let sources: { registry: OutboundCredentialRegistry; configured: ConfiguredCredentials } | undefined;
127
+ const allowed = (field: keyof UpstreamErrorDiagnostics): string | undefined => {
128
+ const value = logCtx[field];
129
+ if (!value) return undefined;
130
+ sources ??= { registry: outboundCredentials(), configured: configuredCredentials() };
131
+ return diagnosticValueAllowed(value, sources.registry, sources.configured) ? value : undefined;
132
+ };
133
+ const code = allowed("upstreamErrorCode");
134
+ const type = allowed("upstreamErrorType");
135
+ const requestId = allowed("upstreamRequestId");
97
136
  return {
98
- ...(logCtx.upstreamErrorCode ? { upstreamErrorCode: logCtx.upstreamErrorCode } : {}),
99
- ...(logCtx.upstreamErrorType ? { upstreamErrorType: logCtx.upstreamErrorType } : {}),
100
- ...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
137
+ ...(code ? { upstreamErrorCode: code } : {}),
138
+ ...(type ? { upstreamErrorType: type } : {}),
139
+ ...(requestId ? { upstreamRequestId: requestId } : {}),
101
140
  };
102
141
  }
103
142
 
143
+ /**
144
+ * The server status a Codex WebSocket `error` frame states on its wrapper ({@link codexWsWrapperStatus}),
145
+ * or undefined. Only 5xx is returned. Every 5xx is the same transient class for account health, so
146
+ * this changes what a row says, not where the next turn goes; a wrapper 4xx would move quota and
147
+ * credential routing and is left to its own review (#6740).
148
+ */
149
+ export function wrappedServerErrorStatus(json: UpstreamErrorPayload): number | undefined {
150
+ const status = codexWsWrapperStatus(json as Record<string, unknown>);
151
+ return status !== undefined && status >= 500 ? status : undefined;
152
+ }
153
+
104
154
  function captureTerminalHttpStatus(
105
155
  logCtx: TerminalStatusContext,
106
156
  json: UpstreamErrorPayload,
@@ -170,8 +220,15 @@ function captureTerminalHttpStatus(
170
220
  code: candidates.find(candidate => typeof candidate?.code === "string")?.code,
171
221
  type: candidates.slice(0, -1).find(candidate => typeof candidate?.type === "string")?.type,
172
222
  });
173
- // Unrecognized bare errors leave the status to explicit event fields (combo preflight).
174
- if (structured) noteStatus(structured.httpStatus);
223
+ if (structured) {
224
+ noteStatus(structured.httpStatus);
225
+ return;
226
+ }
227
+ // An unrecognized class keeps only the status the upstream itself stated on the WebSocket
228
+ // wrapper. Nothing is invented here: with no wrapper status the row stays the synthetic 502, and
229
+ // combo preflight still reads its wider set of explicit event fields after this one.
230
+ const wrapped = wrappedServerErrorStatus(json);
231
+ if (wrapped !== undefined) noteStatus(wrapped);
175
232
  return;
176
233
  }
177
234
  if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;