@bitkyc08/opencodex 2.48.0 → 2.50.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 (141) hide show
  1. package/AGENTS_INSTALL.md +9 -1
  2. package/README.md +11 -5
  3. package/SPONSORS.md +1 -1
  4. package/assets/sponsors/orcarouter.png +0 -0
  5. package/assets/sponsors/packycode.png +0 -0
  6. package/gui/dist/assets/index-BoBRSehJ.css +1 -0
  7. package/gui/dist/assets/index-C39tnjXO.js +115 -0
  8. package/gui/dist/index.html +2 -2
  9. package/gui/dist/provider-icons/packycode.svg +19 -0
  10. package/gui/dist/provider-icons/qoder.svg +5 -0
  11. package/package.json +5 -3
  12. package/src/adapters/anthropic.ts +31 -16
  13. package/src/adapters/codebuddy/adapter.ts +85 -0
  14. package/src/adapters/codebuddy/profiles.ts +52 -0
  15. package/src/adapters/coding-agent/profile.ts +100 -0
  16. package/src/adapters/coding-agent/protocol.ts +463 -0
  17. package/src/adapters/coding-agent/turn.ts +353 -0
  18. package/src/adapters/google.ts +15 -11
  19. package/src/adapters/mimo-free.ts +3 -0
  20. package/src/adapters/openai-chat.ts +2 -2
  21. package/src/adapters/openai-responses.ts +18 -11
  22. package/src/adapters/qoder/adapter.ts +70 -0
  23. package/src/adapters/qoder/live-models.ts +89 -0
  24. package/src/adapters/qoder/profiles.ts +36 -0
  25. package/src/adapters/registry.ts +12 -0
  26. package/src/adapters/responses-tool-schema.ts +113 -8
  27. package/src/claude/inbound.ts +17 -5
  28. package/src/cli/account-api.ts +18 -3
  29. package/src/cli/account-auth.ts +8 -1
  30. package/src/cli/account-extended.ts +2 -1
  31. package/src/cli/account.ts +1 -0
  32. package/src/cli/capabilities.ts +15 -1
  33. package/src/cli/dispatch.ts +2 -0
  34. package/src/cli/doctor.ts +40 -0
  35. package/src/cli/effort.ts +24 -8
  36. package/src/cli/help.ts +2 -0
  37. package/src/cli/index.ts +29 -2
  38. package/src/cli/models-runtime.ts +8 -3
  39. package/src/cli/observe.ts +13 -3
  40. package/src/cli/provider-runtime.ts +2 -1
  41. package/src/cli/registry.ts +2 -2
  42. package/src/cli/system-command.ts +10 -3
  43. package/src/cli/usage-report.ts +9 -5
  44. package/src/clients/config-export/zcode.ts +24 -0
  45. package/src/codex/account-lifecycle.ts +35 -2
  46. package/src/codex/account-runtime-state.ts +6 -1
  47. package/src/codex/account-store.ts +72 -9
  48. package/src/codex/account-usability.ts +3 -2
  49. package/src/codex/auth-api.ts +113 -26
  50. package/src/codex/auth-collision.ts +12 -2
  51. package/src/codex/auth-context.ts +96 -7
  52. package/src/codex/catalog/parsing.ts +23 -0
  53. package/src/codex/catalog/provider-fetch.ts +144 -11
  54. package/src/codex/catalog/sync.ts +14 -0
  55. package/src/codex/inject.ts +128 -30
  56. package/src/codex/internal/catalog-writer.ts +3 -0
  57. package/src/codex/journal.ts +61 -12
  58. package/src/codex/model-cache.ts +11 -4
  59. package/src/codex/native-profile-startup.ts +72 -5
  60. package/src/codex/native-profile-store.ts +2 -2
  61. package/src/codex/ocx-compaction-history.ts +226 -0
  62. package/src/codex/project-config-warnings.ts +3 -1
  63. package/src/codex/quota-auto-refresh.ts +6 -1
  64. package/src/codex/quota.ts +71 -15
  65. package/src/codex/reserve-availability.ts +21 -5
  66. package/src/codex/runtime.ts +45 -1
  67. package/src/codex/sync.ts +5 -0
  68. package/src/combos/index.ts +2 -0
  69. package/src/combos/resolve.ts +52 -0
  70. package/src/config.ts +59 -0
  71. package/src/generated/compatibility-version.json +178 -114
  72. package/src/images/loop.ts +1 -0
  73. package/src/images/xai-video-client.ts +2 -0
  74. package/src/integrations/registry.ts +1 -0
  75. package/src/lib/errors.ts +8 -0
  76. package/src/lib/privacy.ts +25 -0
  77. package/src/lib/process-control.ts +52 -8
  78. package/src/lib/upstream-retry.ts +1 -0
  79. package/src/oauth/chatgpt.ts +83 -0
  80. package/src/oauth/health.ts +47 -12
  81. package/src/oauth/index.ts +46 -8
  82. package/src/oauth/token-guardian.ts +32 -6
  83. package/src/oauth/xai.ts +151 -8
  84. package/src/providers/api-key-selection-capture.ts +10 -0
  85. package/src/providers/api-key-selection.ts +2 -7
  86. package/src/providers/caller-authorization.ts +36 -0
  87. package/src/providers/codebuddy-models.ts +184 -0
  88. package/src/providers/derive.ts +5 -0
  89. package/src/providers/free-directory.ts +26 -2
  90. package/src/providers/google-ai-studio-model-discovery.ts +74 -0
  91. package/src/providers/openai-sidecar.ts +35 -11
  92. package/src/providers/opencode-zen-rate-limit.ts +75 -0
  93. package/src/providers/qoder-models.ts +25 -0
  94. package/src/providers/quota.ts +15 -0
  95. package/src/providers/registry.ts +140 -1
  96. package/src/responses/compaction.ts +4 -0
  97. package/src/responses/task-input.ts +21 -1
  98. package/src/router.ts +1 -1
  99. package/src/server/auth-cors.ts +6 -0
  100. package/src/server/chat-completions.ts +30 -13
  101. package/src/server/chat-native.ts +10 -1
  102. package/src/server/claude-messages.ts +17 -7
  103. package/src/server/images.ts +3 -2
  104. package/src/server/index.ts +25 -2
  105. package/src/server/management/account-selection-stream.ts +13 -4
  106. package/src/server/management/config-routes.ts +24 -5
  107. package/src/server/management/logs-usage-routes.ts +5 -1
  108. package/src/server/management/model-rows.ts +16 -1
  109. package/src/server/management/native-integration-routes.ts +2 -1
  110. package/src/server/management/oauth-account-routes.ts +6 -2
  111. package/src/server/management/provider-routes.ts +33 -2
  112. package/src/server/management/request-history-routes.ts +4 -2
  113. package/src/server/management/route-registry.ts +5 -4
  114. package/src/server/management/shared.ts +66 -3
  115. package/src/server/management-api.ts +15 -1
  116. package/src/server/port-reclaim.ts +11 -26
  117. package/src/server/request-decompress.ts +91 -3
  118. package/src/server/request-log.ts +16 -0
  119. package/src/server/responses/codex-ws-wire.ts +1 -1
  120. package/src/server/responses/collaboration.ts +4 -9
  121. package/src/server/responses/compact.ts +8 -2
  122. package/src/server/responses/context-overflow.ts +11 -0
  123. package/src/server/responses/core.ts +285 -57
  124. package/src/server/responses/fetch-helpers.ts +18 -7
  125. package/src/server/responses/policy-fallback.ts +18 -2
  126. package/src/server/search.ts +2 -2
  127. package/src/service.ts +128 -9
  128. package/src/storage/cleanup.ts +77 -45
  129. package/src/types/accounts.ts +18 -0
  130. package/src/types/config.ts +43 -1
  131. package/src/types/provider.ts +56 -0
  132. package/src/types.ts +4 -0
  133. package/src/usage/log.ts +24 -0
  134. package/src/vision/anthropic-describe.ts +1 -0
  135. package/src/web-search/anthropic-executor.ts +1 -0
  136. package/src/web-search/loop.ts +1 -0
  137. package/src/web-search/ollama-executor.ts +127 -0
  138. package/src/web-search/passthrough-bridge.ts +761 -0
  139. package/src/web-search/progress-stream.ts +4 -0
  140. package/gui/dist/assets/index-B5r7LNHN.js +0 -115
  141. package/gui/dist/assets/index-D5SiRo8X.css +0 -1
@@ -102,7 +102,7 @@ import {
102
102
  } from "../../lib/errors";
103
103
  import { injectionDebugLog } from "../../lib/injection-debug-log";
104
104
  import { resolveClientRetryAfter } from "../../lib/retry-after";
105
- import { enrichOpenCodeZenRateLimitMessage } from "../../providers/opencode-zen-rate-limit";
105
+ import { enrichOpenCodeZenUpstreamMessage } from "../../providers/opencode-zen-rate-limit";
106
106
  import { CODE_MODE_EXEC_TOOL_NAME, modelInList, namespacedToolName } from "../../types";
107
107
  import type {
108
108
  AdapterEvent,
@@ -150,6 +150,11 @@ import {
150
150
  } from "../../oauth/generic-account-failover";
151
151
  import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
152
152
  import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
153
+ import {
154
+ createOllamaBridgeExecutor,
155
+ createPassthroughWebSearchBridgeStream,
156
+ planPassthroughWebSearchBridge,
157
+ } from "../../web-search/passthrough-bridge";
153
158
  import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
154
159
  import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
155
160
  import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
@@ -210,11 +215,14 @@ import {
210
215
  import {
211
216
  ForwardAdmissionCredentialError,
212
217
  hasForwardableCodexBearer,
218
+ isProxyAdmissionSecret,
213
219
  validateForwardAdmissionCredential,
214
220
  } from "../auth-cors";
215
221
  import type { DataPlaneAdmission } from "../auth-cors";
216
222
  import { createTranslatorBudget, isTranslatorBudgetExceededError, type TranslatorBudget } from "../../lib/translator-budget";
217
- import { listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
223
+ import { captureExplicitOpenAiCallerAuth, listOpenAiForwardSidecarCandidates, resolveFirstUsableOpenAiSidecar, type ExplicitOpenAiCallerAuth, type ResolvedOpenAiForwardSidecar } from "../../providers/openai-sidecar";
224
+ import { inspectChatGptDomainClaim } from "../../oauth/chatgpt";
225
+ import { captureCallerDirectAuth, providerConsumesCallerAuthorization, type CallerDirectAuth } from "../../providers/caller-authorization";
218
226
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
219
227
  import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../../codex/loopback-target";
220
228
  import { providerContextCap } from "../../providers/context-cap";
@@ -240,7 +248,13 @@ import { hasPassiveAccountQuota, recordAnthropicAccountQuotaFromHeaders, recordP
240
248
  import { captureConfigGeneration } from "../../lib/state-store-sweeper";
241
249
  import { applyOpenAiVirtualModel, resolveOpenAiCompactModel } from "../../providers/openai-virtual-models";
242
250
  import { isUsageDebugEnabled } from "../../usage/debug";
243
- import { readJsonRequestBody, DecompressedBodyTooLargeError, UnsupportedContentEncodingError } from "../request-decompress";
251
+ import {
252
+ readJsonRequestBody,
253
+ describeInboundBodyRefusal,
254
+ resolveInboundBodyLimitBytes,
255
+ DecompressedBodyTooLargeError,
256
+ UnsupportedContentEncodingError,
257
+ } from "../request-decompress";
244
258
  import { resolveAdapter, resolveWireProtocolOverride } from "../adapter-resolve";
245
259
  import {
246
260
  providerModelResponsesTerminalRepair,
@@ -421,7 +435,7 @@ import {
421
435
  } from "../responses-undeclared-tool-guard";
422
436
  import { createGithubCopilotResponsesBlockRewrite } from "../github-copilot-responses-repair";
423
437
  import { responsesJsonToSseStream } from "../responses-json-events";
424
- import { streamingContextOverflowResponse } from "./context-overflow";
438
+ import { jsonContextOverflowResponse, streamingContextOverflowResponse } from "./context-overflow";
425
439
  import { guardTerminalEventStream } from "./terminal-guard";
426
440
  import {
427
441
  emptyCompletionRetryEnabled,
@@ -1009,14 +1023,14 @@ export function usesCodexForwardPoolAuth(
1009
1023
  && provider.authMode === "forward" && provider.adapter === "openai-responses";
1010
1024
  }
1011
1025
 
1012
- function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig): CodexWsQuotaObserver | undefined {
1026
+ function codexWsQuotaObserver(authCtx: CodexAuthContext, provider: OcxProviderConfig, modelId?: string): CodexWsQuotaObserver | undefined {
1013
1027
  if (!isCanonicalOpenAiForwardProvider(provider) || !usesCodexForwardPoolAuth(authCtx, provider)) return undefined;
1014
1028
  const { accountId, writerGeneration } = authCtx;
1015
1029
  const credentialGeneration = authCtx.kind === "pool" ? authCtx.generation : undefined;
1016
1030
  const mainWriter = authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined;
1017
1031
  return headers => {
1018
1032
  if (credentialGeneration !== undefined && !isCodexAccountGenerationLive(accountId, credentialGeneration)) return;
1019
- applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter);
1033
+ applyCapturedCodexQuota(accountId, headers, writerGeneration, mainWriter, { modelId });
1020
1034
  };
1021
1035
  }
1022
1036
 
@@ -1121,7 +1135,8 @@ export async function shouldRetryCodexPoolAccountQuota(
1121
1135
  }
1122
1136
 
1123
1137
  interface CodexPoolAccountRetryArgs {
1124
- req: Request;
1138
+ /** Sanitized caller input, before any selected Pool credential was materialized. */
1139
+ callerAuthHeaders: Headers;
1125
1140
  config: OcxConfig;
1126
1141
  route: { providerName: string; modelId: string; provider: OcxProviderConfig };
1127
1142
  parsed: OcxParsedRequest;
@@ -1287,7 +1302,7 @@ async function retryCodexPoolOnAlternateAccount(
1287
1302
  args: CodexPoolAccountRetryArgs,
1288
1303
  ): Promise<CodexPoolAccountRetryResult> {
1289
1304
  const {
1290
- req, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
1305
+ callerAuthHeaders, config, route, parsed, logCtx, options, firstAuthCtx, firstResponse,
1291
1306
  outcomeStatus, upstream, connectMs, passthroughEstimate, stream,
1292
1307
  } = args;
1293
1308
  const inboundWire = options.inboundWire ?? "responses";
@@ -1321,7 +1336,7 @@ async function retryCodexPoolOnAlternateAccount(
1321
1336
  }
1322
1337
  try {
1323
1338
  retryAuthCtx ??= await resolveCodexAuthContext(
1324
- req.headers,
1339
+ callerAuthHeaders,
1325
1340
  config,
1326
1341
  "pool",
1327
1342
  {
@@ -1329,7 +1344,7 @@ async function retryCodexPoolOnAlternateAccount(
1329
1344
  admission: options.admission,
1330
1345
  codexAuthPolicy: options.codexAuthPolicy,
1331
1346
  modelId: route.modelId,
1332
- requestScopedMainCredential: hasForwardableCodexBearer(req.headers, config),
1347
+ requestScopedMainCredential: hasForwardableCodexBearer(callerAuthHeaders, config),
1333
1348
  beginCodexAccountSelection: codexAccountSelectionForTurn(options.turnAdmissionLease),
1334
1349
  resolveCodexModelEntitlements: entitlementResolver,
1335
1350
  },
@@ -1378,6 +1393,7 @@ async function retryCodexPoolOnAlternateAccount(
1378
1393
  firstResponse.headers,
1379
1394
  firstAuthCtx.writerGeneration,
1380
1395
  firstAuthCtx.kind === "main-pool" ? firstAuthCtx.mainQuotaWriter : undefined,
1396
+ { modelId: route.modelId },
1381
1397
  );
1382
1398
  }
1383
1399
  const deferFirstOutcome = shouldDeferCodexResetDerivedCooldown(
@@ -1399,7 +1415,7 @@ async function retryCodexPoolOnAlternateAccount(
1399
1415
  // Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
1400
1416
  // ordinary requests must block the first account before the alternate send.
1401
1417
  if (!deferFirstOutcome) recordFirstOutcome();
1402
- const retryHeaders = headersForCodexAuthContext(req.headers, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
1418
+ const retryHeaders = headersForCodexAuthContext(callerAuthHeaders, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
1403
1419
  const retryProvider = applyCodexAuthContextToProvider(
1404
1420
  stripCodexRuntimeProviderFields(route.provider),
1405
1421
  retryAuthCtx,
@@ -1469,7 +1485,7 @@ async function retryCodexPoolOnAlternateAccount(
1469
1485
  providerFetch(route.provider, options.codexWsRuntimeIdentity, {
1470
1486
  providerName: route.providerName,
1471
1487
  modelId: route.modelId,
1472
- onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider),
1488
+ onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
1473
1489
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
1474
1490
  ? createCodexReserveDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
1475
1491
  }),
@@ -1603,7 +1619,7 @@ export function decodeRequestErrorResponse(err: unknown, label: string): Respons
1603
1619
  return formatErrorResponse(415, "invalid_request_error", err.message);
1604
1620
  }
1605
1621
  if (err instanceof DecompressedBodyTooLargeError) {
1606
- return formatErrorResponse(413, "invalid_request_error", err.message);
1622
+ return formatErrorResponse(413, "inbound_body_too_large", describeInboundBodyRefusal(err));
1607
1623
  }
1608
1624
  console.warn(`[${label}] request body decode/parse failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
1609
1625
  return formatErrorResponse(400, "invalid_request_error", "Invalid JSON body");
@@ -1703,9 +1719,17 @@ export interface HandleResponsesOptions {
1703
1719
  /**
1704
1720
  * Claude replay may add native-main auth so OpenAI sidecars remain available.
1705
1721
  * Strip only that internal credential when the final route is a noncanonical
1706
- * forward destination; final routing can differ from Claude's preflight route.
1722
+ * forward/caller-auth destination; final routing can differ from Claude's preflight route.
1707
1723
  */
1708
1724
  stripClaudeMainAuthForNoncanonicalForward?: boolean;
1725
+ /** In-memory credential proven by Claude's native-main turn claim; never persist or log. */
1726
+ trustedClaudeMainAuth?: { authorization: string; chatgptAccountId?: string };
1727
+ /** Sidecar-only auth captured before route changes; null means no usable original pair. */
1728
+ openAiSidecarAuth?: ExplicitOpenAiCallerAuth | null;
1729
+ /** Original caller-owned native pair; separate from any claimed sidecar enrichment. */
1730
+ nativeCallerAuth?: ExplicitOpenAiCallerAuth | null;
1731
+ /** Caller Direct credential under Direct\'s own predicate; restored only for the canonical OpenAI final route. */
1732
+ callerDirectAuth?: CallerDirectAuth | null;
1709
1733
  /** Internal recursion guard; callers outside this module must not set it. */
1710
1734
  comboAttempt?: boolean;
1711
1735
  /** Internal combo handoff for one parent-validated continuation snapshot. */
@@ -1925,6 +1949,9 @@ export function createChildPassthroughCallbackGate(options: HandleResponsesOptio
1925
1949
 
1926
1950
  export function buildComboChildHeaders(parentHeaders: HeadersInit): Headers {
1927
1951
  const childHeaders = new Headers(parentHeaders);
1952
+ // A provisional caller credential is not authoritative for a Combo child.
1953
+ childHeaders.delete("authorization");
1954
+ childHeaders.delete("chatgpt-account-id");
1928
1955
  // Combo children re-serialize already-decoded JSON. Keeping transport metadata from
1929
1956
  // the parent would make the child decoder treat plain JSON as compressed bytes.
1930
1957
  childHeaders.delete("content-length");
@@ -2009,7 +2036,7 @@ function canPassThroughEncryptedV2AgentTask(
2009
2036
  }
2010
2037
 
2011
2038
  type ResponsesAuthResolution =
2012
- | { ok: true; authCtx: CodexAuthContext; headers: Headers; substituteMainCredential: boolean }
2039
+ | { ok: true; authCtx: CodexAuthContext; headers: Headers; callerAuthHeaders: Headers; substituteMainCredential: boolean }
2013
2040
  | { ok: false; response: Response };
2014
2041
 
2015
2042
  /**
@@ -2021,8 +2048,75 @@ async function resolveResponsesCodexAuth(
2021
2048
  config: OcxConfig,
2022
2049
  route: RouteResult,
2023
2050
  options: HandleResponsesOptions,
2051
+ credentialDomainWasRewritten = false,
2024
2052
  ): Promise<ResponsesAuthResolution> {
2025
2053
  try {
2054
+ const routeMayChangeCredentialDomain = options.comboAttempt === true
2055
+ || route.routeKind === "policy"
2056
+ || credentialDomainWasRewritten;
2057
+ const trustedClaudeMainForFinalRoute = options.stripClaudeMainAuthForNoncanonicalForward === true
2058
+ && isCanonicalOpenAiForwardProvider(route.provider)
2059
+ ? options.trustedClaudeMainAuth : undefined;
2060
+ let authInputHeaders = req.headers;
2061
+ // Route-changing recursion retains typed admission, never an unscoped raw
2062
+ // caller credential. Bearer admission is substituted or stripped below.
2063
+ if (routeMayChangeCredentialDomain && options.admission?.source !== "bearer"
2064
+ && !trustedClaudeMainForFinalRoute) {
2065
+ authInputHeaders = new Headers(req.headers);
2066
+ authInputHeaders.delete("authorization");
2067
+ authInputHeaders.delete("chatgpt-account-id");
2068
+ }
2069
+ if (trustedClaudeMainForFinalRoute) {
2070
+ authInputHeaders = new Headers(authInputHeaders);
2071
+ authInputHeaders.set("authorization", trustedClaudeMainForFinalRoute.authorization);
2072
+ if (trustedClaudeMainForFinalRoute.chatgptAccountId) {
2073
+ authInputHeaders.set("chatgpt-account-id", trustedClaudeMainForFinalRoute.chatgptAccountId);
2074
+ } else {
2075
+ authInputHeaders.delete("chatgpt-account-id");
2076
+ }
2077
+ }
2078
+ // A caller-auth transport that is not canonical OpenAI (keyless Cursor) consumes the
2079
+ // caller's Authorization as its own upstream token. Keep that contract only for a clean
2080
+ // single bearer with NO ChatGPT-domain marker. A bearer marked for the ChatGPT domain —
2081
+ // whether its marker is valid or malformed/conflicting — a combined/malformed value, or
2082
+ // the captured explicit OpenAI pair is never a Cursor token; a foreign JWT carrying only
2083
+ // a generic organizations claim is not ChatGPT-marked and keeps the legacy contract.
2084
+ // chatgpt-account-id has no meaning outside the ChatGPT domain.
2085
+ if (!isCanonicalOpenAiForwardProvider(route.provider)
2086
+ && providerConsumesCallerAuthorization(route.provider)) {
2087
+ const rawAuth = authInputHeaders.get("authorization")?.trim();
2088
+ const singleBearer = /^Bearer[\t ]+([^\s,]+)$/i.exec(rawAuth ?? "")?.[1];
2089
+ const domainClaim = singleBearer ? inspectChatGptDomainClaim(singleBearer) : { kind: "absent" as const };
2090
+ const dropBearer = options.nativeCallerAuth != null || domainClaim.kind !== "absent"
2091
+ || (rawAuth !== undefined && singleBearer === undefined);
2092
+ if (dropBearer || authInputHeaders.has("chatgpt-account-id")) {
2093
+ const scoped = new Headers(authInputHeaders);
2094
+ if (dropBearer) scoped.delete("authorization");
2095
+ scoped.delete("chatgpt-account-id");
2096
+ authInputHeaders = scoped;
2097
+ }
2098
+ }
2099
+ // The caller's own Direct credential may cross an internal route change only to the
2100
+ // canonical OpenAI transport, under a predicate deliberately STRICTER than plain
2101
+ // unchanged-route Direct forwarding: a clean non-proxy bearer whose ChatGPT-domain
2102
+ // marker is valid, with any explicit account header matching that marker. Unchanged
2103
+ // routes keep their legacy rules; sidecar enrichment grants no primary authority.
2104
+ if (options.callerDirectAuth && isCanonicalOpenAiForwardProvider(route.provider)) {
2105
+ const directHeaders = new Headers({
2106
+ authorization: options.callerDirectAuth.authorization,
2107
+ ...(options.callerDirectAuth.chatgptAccountId
2108
+ ? { "chatgpt-account-id": options.callerDirectAuth.chatgptAccountId } : {}),
2109
+ });
2110
+ if (captureCallerDirectAuth(directHeaders, config)) {
2111
+ authInputHeaders = new Headers(authInputHeaders);
2112
+ authInputHeaders.set("authorization", options.callerDirectAuth.authorization);
2113
+ if (options.callerDirectAuth.chatgptAccountId) {
2114
+ authInputHeaders.set("chatgpt-account-id", options.callerDirectAuth.chatgptAccountId);
2115
+ } else {
2116
+ authInputHeaders.delete("chatgpt-account-id");
2117
+ }
2118
+ }
2119
+ }
2026
2120
  // #1686: a caller that proved admission with a BEARER presented one of our own secrets.
2027
2121
  // Refusing it here is what made the codex-cli `env_key` contract unusable against Direct.
2028
2122
  // Admitting it is only safe because the stored main credential is substituted below, so
@@ -2046,15 +2140,16 @@ async function resolveResponsesCodexAuth(
2046
2140
  // no-ChatGPT-login install keeps working.
2047
2141
  const substituteMainCredential = options.admission?.source === "bearer"
2048
2142
  && (route.codexAccountMode !== undefined || isCanonicalOpenAiForwardProvider(route.provider));
2143
+ const stripAuthorization = options.admission?.source === "bearer" && !substituteMainCredential;
2049
2144
  const requestScopedMainCredential = route.codexAccountMode !== undefined
2050
2145
  && !substituteMainCredential
2051
- && hasForwardableCodexBearer(req.headers, config);
2146
+ && hasForwardableCodexBearer(authInputHeaders, config);
2052
2147
  if (route.codexAccountMode === "direct" && !substituteMainCredential) {
2053
- validateForwardAdmissionCredential(req.headers, config);
2148
+ validateForwardAdmissionCredential(authInputHeaders, config);
2054
2149
  }
2055
2150
  let authCtx: CodexAuthContext;
2056
2151
  if (route.codexAccountMode) {
2057
- authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, {
2152
+ authCtx = await resolveCodexAuthContext(authInputHeaders, config, route.codexAccountMode, {
2058
2153
  admission: options.admission,
2059
2154
  codexAuthPolicy: options.codexAuthPolicy,
2060
2155
  accountId: route.codexAccountId,
@@ -2090,7 +2185,7 @@ async function resolveResponsesCodexAuth(
2090
2185
  // (custom-named canonical-forward providers must retain the same protection).
2091
2186
  const mainPolicyConfig = isCanonicalOpenAiForwardProvider(route.provider)
2092
2187
  ? options.codexAuthPolicy ?? config : undefined;
2093
- const headers = await materializeCodexUpstreamAuthAsync(req.headers, authCtx, {
2188
+ const headers = await materializeCodexUpstreamAuthAsync(authInputHeaders, authCtx, {
2094
2189
  admission: options.admission,
2095
2190
  config: mainPolicyConfig,
2096
2191
  modelId: route.modelId,
@@ -2109,10 +2204,27 @@ async function resolveResponsesCodexAuth(
2109
2204
  response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
2110
2205
  };
2111
2206
  }
2207
+ if (stripAuthorization) {
2208
+ headers.delete("authorization");
2209
+ headers.delete("chatgpt-account-id");
2210
+ }
2211
+ if (providerConsumesCallerAuthorization(route.provider) && options.admission?.source !== undefined
2212
+ && options.admission.source !== "loopback") {
2213
+ validateForwardAdmissionCredential(headers, config);
2214
+ } else {
2215
+ // Even adapters that ignore caller auth must not retain a proxy secret for
2216
+ // a later internal hop or a future transport change.
2217
+ const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
2218
+ if (bearer && isProxyAdmissionSecret(bearer, config)) {
2219
+ headers.delete("authorization");
2220
+ headers.delete("chatgpt-account-id");
2221
+ }
2222
+ }
2112
2223
  return {
2113
2224
  ok: true,
2114
2225
  authCtx,
2115
2226
  headers,
2227
+ callerAuthHeaders: new Headers(authInputHeaders),
2116
2228
  substituteMainCredential,
2117
2229
  };
2118
2230
  } catch (err) {
@@ -2679,6 +2791,10 @@ export async function handleComboResponses(
2679
2791
  logCtx.routeDecision = comboRouteDecisionTrace(config, comboId, pick, requestedModel);
2680
2792
 
2681
2793
  let lastFailure: Response | null = null;
2794
+ // The exhausted-combo mapping below runs outside the loop, where `failure.upstreamCode`
2795
+ // is gone, so carry the loop's own classification decision instead of re-deriving a
2796
+ // weaker one from the status alone (#4149).
2797
+ let lastFailureClassifiesOverflow = false;
2682
2798
  while (pick) {
2683
2799
  if (options.abortSignal?.aborted) return clientCancelledResponse();
2684
2800
  const childLog: RequestLogContext = {
@@ -2886,6 +3002,12 @@ export async function handleComboResponses(
2886
3002
  const failureDecision = comboFailureDecision(failure.response.status, failure.classificationText, {
2887
3003
  code: failure.upstreamCode,
2888
3004
  });
3005
+ const wantsStream = (rawBody as { stream?: unknown } | null)?.stream === true;
3006
+ // Local byte admission has its own diagnostic; do not relabel it as an upstream refusal.
3007
+ const classifyOverflow = failure.response.status === 413
3008
+ && (wantsStream || (failure.upstreamCode !== "outbound_body_too_large"
3009
+ && failure.upstreamCode !== "translation_buffer_limit"));
3010
+ lastFailureClassifiesOverflow = classifyOverflow;
2889
3011
  if (storedPool401ReplayDispatched) {
2890
3012
  if (failureDecision === "hop" && unreadableEncryptedAgentTask && !comboPayloadReadable) {
2891
3013
  const recoveredTarget = await pickWithWait({
@@ -2910,15 +3032,19 @@ export async function handleComboResponses(
2910
3032
  // Keep the spent Pool budget sticky even after a recovered routed child:
2911
3033
  // no later failure may reopen ordinary combo/native account hopping.
2912
3034
  adoptFailedChildLog(childLog);
3035
+ if (classifyOverflow && failureDecision === "stop") {
3036
+ return wantsStream
3037
+ ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
3038
+ : jsonContextOverflowResponse();
3039
+ }
2913
3040
  return lastFailure;
2914
3041
  }
2915
3042
  if (failureDecision === "stop") {
2916
3043
  adoptFailedChildLog(childLog);
2917
- if (
2918
- failure.response.status === 413
2919
- && (rawBody as { stream?: unknown } | null)?.stream === true
2920
- ) {
2921
- return streamingContextOverflowResponse(requestedModel, options.translatorBudget);
3044
+ if (classifyOverflow) {
3045
+ return wantsStream
3046
+ ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
3047
+ : jsonContextOverflowResponse();
2922
3048
  }
2923
3049
  return lastFailure;
2924
3050
  }
@@ -2968,9 +3094,11 @@ export async function handleComboResponses(
2968
3094
  }
2969
3095
  if (
2970
3096
  lastFailure?.status === 413
2971
- && (rawBody as { stream?: unknown } | null)?.stream === true
3097
+ && lastFailureClassifiesOverflow
2972
3098
  ) {
2973
- return streamingContextOverflowResponse(requestedModel, options.translatorBudget);
3099
+ return (rawBody as { stream?: unknown } | null)?.stream === true
3100
+ ? streamingContextOverflowResponse(requestedModel, options.translatorBudget)
3101
+ : jsonContextOverflowResponse();
2974
3102
  }
2975
3103
  return lastFailure!;
2976
3104
  }
@@ -3084,6 +3212,12 @@ export async function handleResponses(
3084
3212
  try {
3085
3213
  const response = await handleResponsesInner(req, config, logCtx, {
3086
3214
  ...options,
3215
+ openAiSidecarAuth: options.openAiSidecarAuth === undefined
3216
+ ? captureExplicitOpenAiCallerAuth(req.headers, config) : options.openAiSidecarAuth,
3217
+ nativeCallerAuth: options.nativeCallerAuth === undefined
3218
+ ? captureExplicitOpenAiCallerAuth(req.headers, config) : options.nativeCallerAuth,
3219
+ callerDirectAuth: options.callerDirectAuth === undefined
3220
+ ? captureCallerDirectAuth(req.headers, config) : options.callerDirectAuth,
3087
3221
  // Capture before combo replay rebuilds the Request headers; children carry options.
3088
3222
  visionDescribeTerminal: options.visionDescribeTerminal === true
3089
3223
  || req.headers.get("x-opencodex-vision-describe") === "1",
@@ -3116,7 +3250,7 @@ async function handleResponsesInner(
3116
3250
  const agentTaskRecovery = agentTaskRecoveryConfig(config);
3117
3251
  let body: unknown;
3118
3252
  try {
3119
- body = await readJsonRequestBody(req, translatorBudget);
3253
+ body = await readJsonRequestBody(req, translatorBudget, resolveInboundBodyLimitBytes(config.maxInboundBodyBytes));
3120
3254
  } catch (err) {
3121
3255
  if (options.abortSignal?.aborted || req.signal.aborted) {
3122
3256
  return clientCancelledResponse();
@@ -3169,6 +3303,30 @@ async function handleResponsesInner(
3169
3303
  }
3170
3304
  }
3171
3305
  }
3306
+ // A shadow-call replacement that names a COMBO is routing policy, not the identity of any
3307
+ // one pick. The late intercept site below resolves it through routeModel/tryPickComboModel,
3308
+ // which collapses the table to a single target while still tagging `routeKind: "combo"`, so
3309
+ // the combo gate on the next line never fires, handleComboResponses never runs, and 429/5xx
3310
+ // hops — which only exist inside that loop — are unreachable (#4129). Rewrite the selector
3311
+ // here instead, before comboIdFromRawBody reads `model`, and identify the combo by CONFIG
3312
+ // LOOKUP so the check can never observe a one-candidate collapse.
3313
+ if (!options.comboAttempt && body && typeof body === "object" && !Array.isArray(body)) {
3314
+ const shadowIntercept = config.shadowCallIntercept;
3315
+ const rawShadowModel = (body as { model?: unknown }).model;
3316
+ if (shadowIntercept?.enabled && shadowIntercept.model && typeof rawShadowModel === "string"
3317
+ && isShadowSourceModel(rawShadowModel, shadowIntercept.sourceModels)) {
3318
+ const shadowComboId = resolveComboId(config, shadowIntercept.model);
3319
+ if (shadowComboId && Object.hasOwn(config.combos ?? {}, shadowComboId)) {
3320
+ (body as Record<string, unknown>).model = shadowIntercept.model;
3321
+ // Same rule as the late intercept site: record the operator-configured prefix that
3322
+ // matched, never the caller's raw model string. Matching is by prefix, so the raw
3323
+ // value is caller-controlled and reaches usage.jsonl and /api/logs.
3324
+ logCtx.shadowCallRewrittenFrom = sanitizeLogMetadataString(
3325
+ shadowSourceModelPrefix(rawShadowModel, shadowIntercept.sourceModels),
3326
+ );
3327
+ }
3328
+ }
3329
+ }
3172
3330
  const comboId = !options.comboAttempt ? comboIdFromRawBody(body, config) : null;
3173
3331
  if (comboId && Object.hasOwn(config.combos ?? {}, comboId)) {
3174
3332
  options.onRequestBodyRead?.();
@@ -3332,6 +3490,7 @@ async function handleResponsesInner(
3332
3490
  logCtx.configuredSpeedLabel = requestLogSpeedLabel(logCtx.configuredServiceTier);
3333
3491
 
3334
3492
  let route: RouteResult;
3493
+ let credentialDomainWasRewritten = false;
3335
3494
  try {
3336
3495
  // A `compaction_trigger` turn may name a bare native model the operator has
3337
3496
  // no canonical OpenAI route for (#2901). Only the initial compaction route
@@ -3353,6 +3512,7 @@ async function handleResponsesInner(
3353
3512
  } catch { /* Native Codex helper calls remain OpenAI-owned without an enabled OpenAI route. */ }
3354
3513
  const targetRoute = resolveRoute(_sci.model);
3355
3514
  if (shouldInterceptShadowCall(parsed.modelId, _sci.sourceModels, sourceIdentity, targetRoute)) {
3515
+ credentialDomainWasRewritten = true;
3356
3516
  const _sciOriginal = parsed.modelId;
3357
3517
  parsed.modelId = _sci.model;
3358
3518
  if (parsed._rawBody && typeof parsed._rawBody === "object") {
@@ -3484,6 +3644,7 @@ async function handleResponsesInner(
3484
3644
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
3485
3645
  try {
3486
3646
  route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
3647
+ credentialDomainWasRewritten = true;
3487
3648
  logCtx.routeDecision = route.routeDecision;
3488
3649
  } catch (err) {
3489
3650
  if (err instanceof NoAvailableComboTargetsError) {
@@ -3503,10 +3664,19 @@ async function handleResponsesInner(
3503
3664
  let recoveryFailureReason: AgentTaskRecoveryFailureReason | undefined;
3504
3665
  // Native fallback and explicitly trusted direct Responses routes can consume ciphertext,
3505
3666
  // so recover only after final route selection.
3667
+ //
3668
+ // Deliberately NOT gated on `threadSpawn` (#4089). Switching a live thread from a native
3669
+ // ChatGPT model to a routed provider replays a backend-minted encrypted agent message on every
3670
+ // later turn, and a model switch is not a spawn, so the spawn requirement failed the thread
3671
+ // closed permanently without ever attempting recovery. The trust boundary is
3672
+ // `recoveryAdmission()` in ./agent-task-recovery -- Codex originator, live native ChatGPT
3673
+ // bearer, matching chatgpt-account-id, no inbound API key, no proxy-admission secret -- which
3674
+ // admits only the owner of the session that would be spent. `threadSpawn` narrowed which of
3675
+ // that owner's own requests could use their own session; it kept nobody else out. The combo
3676
+ // gate above keeps its spawn requirement: that path has its own native-target filtering and
3677
+ // per-attempt failover, and the reported defect is on this path.
3506
3678
  if (
3507
3679
  inboundWire === "responses"
3508
- &&
3509
- threadSpawn
3510
3680
  && agentTaskRecovery
3511
3681
  && !isCanonicalOpenAiForwardProvider(route.provider)
3512
3682
  && !options.comboAttempt
@@ -3620,6 +3790,7 @@ async function handleResponsesInner(
3620
3790
  if (fallback?.to && !slugsEquivalent(fallback.to, route.modelId)) {
3621
3791
  try {
3622
3792
  route = routeModel(config, fallback.to, evidenceFromBody(parsed._rawBody));
3793
+ credentialDomainWasRewritten = true;
3623
3794
  logCtx.routeDecision = route.routeDecision;
3624
3795
  } catch (err) {
3625
3796
  if (err instanceof NoAvailableComboTargetsError) {
@@ -3742,11 +3913,13 @@ async function handleResponsesInner(
3742
3913
  }
3743
3914
 
3744
3915
  let substituteMainCredential = false;
3916
+ let callerAuthHeaders: Headers;
3745
3917
  {
3746
- const finalAuth = await resolveResponsesCodexAuth(req, config, route, options);
3918
+ const finalAuth = await resolveResponsesCodexAuth(req, config, route, options, credentialDomainWasRewritten);
3747
3919
  if (!finalAuth.ok) return finalAuth.response;
3748
3920
  authCtx = finalAuth.authCtx;
3749
3921
  selectedForwardHeaders = finalAuth.headers;
3922
+ callerAuthHeaders = finalAuth.callerAuthHeaders;
3750
3923
  substituteMainCredential = finalAuth.substituteMainCredential;
3751
3924
  }
3752
3925
 
@@ -4036,7 +4209,8 @@ async function handleResponsesInner(
4036
4209
  const ownsBearer = snapshot !== undefined
4037
4210
  && sentHeaders?.get("authorization") === `Bearer ${snapshot.accessToken}`
4038
4211
  && !sentHeaders?.has("x-api-key");
4039
- const response = await fetchImpl(destination, dispatchInit);
4212
+ // Reselection can choose a provider override instead of the supplied executor.
4213
+ const response = await fetchImpl(destination, { ...dispatchInit, redirect: "manual" });
4040
4214
  // Observe each physical response before retries replace it. The binding belongs to
4041
4215
  // this dispatch, so a manual switch cannot file A's headers against B. Header
4042
4216
  // overrides and credential replacement make ownership unprovable: skip those writes.
@@ -4223,9 +4397,9 @@ async function handleResponsesInner(
4223
4397
  );
4224
4398
  let adapterProvider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire);
4225
4399
  const stripClaudeMainAuth = options.stripClaudeMainAuthForNoncanonicalForward === true
4226
- && adapterProvider.adapter === "openai-responses"
4227
- && adapterProvider.authMode === "forward"
4228
- && !isCanonicalOpenAiForwardProvider(adapterProvider);
4400
+ && !isCanonicalOpenAiForwardProvider(adapterProvider)
4401
+ && ((adapterProvider.adapter === "openai-responses" && adapterProvider.authMode === "forward")
4402
+ || providerConsumesCallerAuthorization(adapterProvider));
4229
4403
  if (stripClaudeMainAuth) {
4230
4404
  releaseCodexAuthContextProbeLease(authCtx);
4231
4405
  authCtx = { kind: "main", accountId: null };
@@ -4313,9 +4487,18 @@ async function handleResponsesInner(
4313
4487
  const needsOpenAiSearch = shouldResolveOpenAiWebSearchSidecar(config, parsed, isPassthrough);
4314
4488
  if (needsOpenAiVision || needsOpenAiSearch) {
4315
4489
  try {
4490
+ // Preserve explicit OpenAI helper auth across route changes without returning it to
4491
+ // primary-provider headers or alternate-main retry. The resolver revalidates scope.
4492
+ const sidecarHeaders = new Headers(req.headers);
4493
+ sidecarHeaders.delete("authorization");
4494
+ sidecarHeaders.delete("chatgpt-account-id");
4495
+ if (options.openAiSidecarAuth) {
4496
+ sidecarHeaders.set("authorization", options.openAiSidecarAuth.authorization);
4497
+ sidecarHeaders.set("chatgpt-account-id", options.openAiSidecarAuth.chatgptAccountId);
4498
+ }
4316
4499
  openAiSidecar = await resolveFirstUsableOpenAiSidecar(
4317
4500
  listOpenAiForwardSidecarCandidates(config),
4318
- req.headers,
4501
+ sidecarHeaders,
4319
4502
  config,
4320
4503
  {
4321
4504
  admission: options.admission,
@@ -4928,7 +5111,7 @@ async function handleResponsesInner(
4928
5111
  dispatchOverride: oauthDispatch(request),
4929
5112
  providerName: route.providerName,
4930
5113
  modelId: route.modelId,
4931
- onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
5114
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
4932
5115
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
4933
5116
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
4934
5117
  }),
@@ -5006,7 +5189,7 @@ async function handleResponsesInner(
5006
5189
  dispatchOverride: oauthDispatch(request),
5007
5190
  providerName: route.providerName,
5008
5191
  modelId: route.modelId,
5009
- onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
5192
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
5010
5193
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
5011
5194
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5012
5195
  }),
@@ -5112,7 +5295,7 @@ async function handleResponsesInner(
5112
5295
  dispatchOverride: oauthDispatch(request),
5113
5296
  providerName: route.providerName,
5114
5297
  modelId: route.modelId,
5115
- onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
5298
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
5116
5299
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
5117
5300
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5118
5301
  }),
@@ -5232,7 +5415,7 @@ async function handleResponsesInner(
5232
5415
  dispatchOverride: oauthDispatch(request),
5233
5416
  providerName: route.providerName,
5234
5417
  modelId: route.modelId,
5235
- onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
5418
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
5236
5419
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
5237
5420
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5238
5421
  }),
@@ -5332,7 +5515,7 @@ async function handleResponsesInner(
5332
5515
  dispatchOverride: oauthDispatch(request),
5333
5516
  providerName: route.providerName,
5334
5517
  modelId: route.modelId,
5335
- onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider),
5518
+ onCodexWsQuota: codexWsQuotaObserver(authCtx, route.provider, route.modelId),
5336
5519
  beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
5337
5520
  ? createCodexReserveDispatchGuard(authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
5338
5521
  }),
@@ -5400,7 +5583,7 @@ async function handleResponsesInner(
5400
5583
  // justify because this flag already produced the identical result.
5401
5584
  const storedReplaySpent = codex401ReplayKind === "stored";
5402
5585
  const retry = await retryCodexPoolOnAlternateAccount({
5403
- req,
5586
+ callerAuthHeaders,
5404
5587
  config,
5405
5588
  route,
5406
5589
  parsed,
@@ -5536,7 +5719,8 @@ async function handleResponsesInner(
5536
5719
  const { applyAccountQuotaFromUpstreamHeaders } = await import("../../codex/auth-api");
5537
5720
  if (!isCodexWsQuotaObservedResponse(upstreamResponse)) {
5538
5721
  applyAccountQuotaFromUpstreamHeaders(authCtx.accountId, upstreamResponse.headers,
5539
- authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined);
5722
+ authCtx.writerGeneration, authCtx.kind === "main-pool" ? authCtx.mainQuotaWriter : undefined,
5723
+ { modelId: route.modelId });
5540
5724
  }
5541
5725
  if (terminalBodyWillRecord) {
5542
5726
  options.setTerminalOutcomeRecorder?.((status, httpStatusOverride) => {
@@ -5577,7 +5761,8 @@ async function handleResponsesInner(
5577
5761
 
5578
5762
  // Non-2xx passthrough failures must never reach Codex as an empty body —
5579
5763
  // Codex renders that as the opaque "Unknown error" (#452). Combo attempts
5580
- // keep their typed failure envelope. Non-empty bodies are relayed verbatim
5764
+ // keep their typed failure envelope. Except for the classified 413 below,
5765
+ // non-empty bodies are relayed verbatim
5581
5766
  // (headers included) so pool-retry Activation B/D and client diagnostics stay intact.
5582
5767
  // Manual-redirect policy (#914): a 3xx is relayed as-is (Location preserved
5583
5768
  // through sanitizePassthroughHeaders) so a redirect to a dead host can never
@@ -5605,11 +5790,10 @@ async function handleResponsesInner(
5605
5790
  // The bounded reader owns the original body, deadline, abort settlement, and lock.
5606
5791
  // Unsafe partial data falls back to #452's non-empty status-only JSON.
5607
5792
  const errorText = await readDisplaySafeErrorText(upstreamResponse, upstream.signal, "");
5608
- if (upstreamResponse.status === 413 && clientRequestedStream) {
5609
- return streamingContextOverflowResponse(
5610
- parsed._responseModelId ?? parsed.modelId,
5611
- translatorBudget,
5612
- );
5793
+ if (upstreamResponse.status === 413) {
5794
+ return clientRequestedStream
5795
+ ? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
5796
+ : jsonContextOverflowResponse();
5613
5797
  }
5614
5798
  return formatPassthroughUpstreamError(upstreamResponse.status, errorText, {
5615
5799
  statusText: upstreamResponse.statusText,
@@ -5638,15 +5822,60 @@ async function handleResponsesInner(
5638
5822
  route.provider,
5639
5823
  route.modelId,
5640
5824
  );
5825
+ // #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
5826
+ // and this branch relays that declaration on the assumption the destination executes it.
5827
+ // A KEY-auth gateway that does not (Ollama Cloud GLM) answers with a function_call named
5828
+ // web_search that nothing runs, and the undeclared-tool guard below ends the turn. When the
5829
+ // provider opts in, the bridge intercepts that one call, runs the search, continues the
5830
+ // conversation upstream, and hands back ordinary Responses SSE — so every rewrite below,
5831
+ // including the guard itself, still inspects the client-facing stream. Default OFF: without
5832
+ // the opt-in this is one planner call and the relay is byte-identical to before.
5833
+ const webSearchBridgePlan = planPassthroughWebSearchBridge(parsed, route.provider, {
5834
+ isPassthrough: true,
5835
+ stream: parsed.stream === true,
5836
+ });
5837
+ // The bridge wraps the RAW upstream body, so terminal repair below still owns the single
5838
+ // client-facing terminal — the bridge drops the terminal of every intercepted leg.
5839
+ const upstreamSseBody = webSearchBridgePlan
5840
+ ? createPassthroughWebSearchBridgeStream({
5841
+ plan: webSearchBridgePlan,
5842
+ firstLeg: upstreamResponse.body,
5843
+ requestBody: request.body,
5844
+ // Continuation legs replay the same built request with the executed search appended.
5845
+ // The first leg already passed the recovery ladder, the outbound size ceiling, and the
5846
+ // host circuit; a KEY-auth destination has no OAuth refresh to replay on a later leg.
5847
+ send: (continuationBody: string) => fetchWithHeaderTimeout(
5848
+ request.url,
5849
+ { method: request.method, headers: request.headers, body: continuationBody },
5850
+ upstream.signal,
5851
+ connectMs,
5852
+ true,
5853
+ providerFetch(route.provider, options.codexWsRuntimeIdentity, {
5854
+ dispatchOverride: oauthDispatch(request),
5855
+ providerName: route.providerName,
5856
+ modelId: route.modelId,
5857
+ }),
5858
+ false,
5859
+ ),
5860
+ execute: createOllamaBridgeExecutor(webSearchBridgePlan, route.provider.apiKey ?? ""),
5861
+ // Appending a search result can push the continuation past the ceiling the first leg
5862
+ // was admitted under, so the same limit is re-applied before every later send.
5863
+ checkOutboundBody: (continuationBody: string) => {
5864
+ const result = checkOutboundBodySize(continuationBody, config.maxUpstreamBodyBytes);
5865
+ return result.admitted ? undefined : describeOutboundBodyRefusal(result);
5866
+ },
5867
+ signal: upstream.signal,
5868
+ })
5869
+ : upstreamResponse.body;
5641
5870
  const passthroughSseBody = terminalRepairPolicy
5642
5871
  ? relayResponsesSseWithTerminalRepair(
5643
- upstreamResponse.body,
5872
+ upstreamSseBody,
5644
5873
  upstream,
5645
5874
  terminalRepairPolicy,
5646
5875
  translatorBudget,
5647
5876
  options.responsesTerminalRepairScheduler,
5648
5877
  )
5649
- : upstreamResponse.body;
5878
+ : upstreamSseBody;
5650
5879
  const repairConfig = route.provider.responsesItemIdRepair;
5651
5880
  // Grok Build renders deltas live but reconstructs its durable assistant
5652
5881
  // turn from the completed response snapshot. Native Responses streams
@@ -7341,11 +7570,10 @@ async function handleResponsesInner(
7341
7570
  } finally {
7342
7571
  cleanupUpstreamAbort();
7343
7572
  }
7344
- if (upstreamResponse.status === 413 && clientRequestedStream && !options.comboAttempt) {
7345
- return streamingContextOverflowResponse(
7346
- parsed._responseModelId ?? parsed.modelId,
7347
- translatorBudget,
7348
- );
7573
+ if (upstreamResponse.status === 413) {
7574
+ return clientRequestedStream
7575
+ ? streamingContextOverflowResponse(parsed._responseModelId ?? parsed.modelId, translatorBudget)
7576
+ : jsonContextOverflowResponse();
7349
7577
  }
7350
7578
  if (!isFixedCodexAccount(authCtx)) {
7351
7579
  recordSubagentQuotaFailureForThreadSpawn(
@@ -7365,7 +7593,7 @@ async function handleResponsesInner(
7365
7593
  const message = normalized.cyberPolicy
7366
7594
  ? normalized.message
7367
7595
  ?? (isCyberPolicyCode(normalized.code) ? CYBER_POLICY_FALLBACK_MESSAGE : normalized.safeText)
7368
- : enrichOpenCodeZenRateLimitMessage(
7596
+ : enrichOpenCodeZenUpstreamMessage(
7369
7597
  `Provider error ${upstreamResponse.status}: ${normalized.safeText}`,
7370
7598
  {
7371
7599
  status: upstreamResponse.status,
@@ -7376,7 +7604,7 @@ async function handleResponsesInner(
7376
7604
  hasApiKey: Boolean(route.provider.apiKey?.trim()),
7377
7605
  upstreamRetryAfter,
7378
7606
  // This recovery path is the HTTP Responses wire; custom runTurn transports
7379
- // never reach enrichOpenCodeZenRateLimitMessage here.
7607
+ // never reach enrichOpenCodeZenUpstreamMessage here.
7380
7608
  supportsHttpSameKeyRetry: true,
7381
7609
  },
7382
7610
  );