@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.
- package/AGENTS_INSTALL.md +9 -1
- package/README.md +11 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +29 -2
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +113 -26
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +96 -7
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +144 -11
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +128 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +71 -15
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +59 -0
- package/src/generated/compatibility-version.json +178 -114
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +140 -1
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +30 -13
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +17 -7
- package/src/server/images.ts +3 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +5 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +33 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +15 -1
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +16 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +285 -57
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +18 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +128 -9
- package/src/storage/cleanup.ts +77 -45
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +43 -1
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B5r7LNHN.js +0 -115
- 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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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, "
|
|
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(
|
|
2146
|
+
&& hasForwardableCodexBearer(authInputHeaders, config);
|
|
2052
2147
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
2053
|
-
validateForwardAdmissionCredential(
|
|
2148
|
+
validateForwardAdmissionCredential(authInputHeaders, config);
|
|
2054
2149
|
}
|
|
2055
2150
|
let authCtx: CodexAuthContext;
|
|
2056
2151
|
if (route.codexAccountMode) {
|
|
2057
|
-
authCtx = await resolveCodexAuthContext(
|
|
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(
|
|
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
|
-
|
|
2919
|
-
|
|
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
|
-
&&
|
|
3097
|
+
&& lastFailureClassifiesOverflow
|
|
2972
3098
|
) {
|
|
2973
|
-
return
|
|
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
|
-
|
|
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
|
|
4227
|
-
&& adapterProvider.authMode === "forward"
|
|
4228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
5609
|
-
return
|
|
5610
|
-
parsed._responseModelId ?? parsed.modelId,
|
|
5611
|
-
|
|
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
|
-
|
|
5872
|
+
upstreamSseBody,
|
|
5644
5873
|
upstream,
|
|
5645
5874
|
terminalRepairPolicy,
|
|
5646
5875
|
translatorBudget,
|
|
5647
5876
|
options.responsesTerminalRepairScheduler,
|
|
5648
5877
|
)
|
|
5649
|
-
:
|
|
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
|
|
7345
|
-
return
|
|
7346
|
-
parsed._responseModelId ?? parsed.modelId,
|
|
7347
|
-
|
|
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
|
-
:
|
|
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
|
|
7607
|
+
// never reach enrichOpenCodeZenUpstreamMessage here.
|
|
7380
7608
|
supportsHttpSameKeyRetry: true,
|
|
7381
7609
|
},
|
|
7382
7610
|
);
|