@bitkyc08/opencodex 2.57.0 → 2.59.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/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createSteeringSettingsNormalizer } from "./native-steering-policy";
|
|
2
|
+
import { nativeResponseControlEligible } from "./native-response-control";
|
|
3
|
+
import { NativeInjectionReplay } from "./native-injection-replay";
|
|
4
|
+
import { NativeSteeringReplay } from "./native-steering-replay";
|
|
1
5
|
import type {
|
|
2
6
|
ResponsesRequestContext,
|
|
3
7
|
ResponsesAdmissionState,
|
|
@@ -7,10 +11,11 @@ import type { PreparedResponsesRequest } from "./request-prepare";
|
|
|
7
11
|
import type { ResponsesTransport } from "./request-transport";
|
|
8
12
|
import type { ResponsesEffects } from "./response-effects";
|
|
9
13
|
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
14
|
+
import { transientSendCapFor } from "./request-send-budget";
|
|
10
15
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
11
16
|
import { codexSafetyBufferingFilterOptions, terminalStatusFromParsed } from "../relay";
|
|
12
17
|
import { imageGenToolCallAliases } from "../responses-image-gen-repair";
|
|
13
|
-
import { rememberResponseState } from "../../responses/state";
|
|
18
|
+
import { rememberResponseState, isBodyNonPersistable } from "../../responses/state";
|
|
14
19
|
import {
|
|
15
20
|
currentTurnWireToolCatalogBody,
|
|
16
21
|
hasExplicitWireToolCatalog,
|
|
@@ -64,6 +69,7 @@ import {
|
|
|
64
69
|
sealRequestAttemptIdentity,
|
|
65
70
|
recordAttemptCredentialSource,
|
|
66
71
|
} from "../request-log";
|
|
72
|
+
import { noteAttemptRecoveryWithheld } from "../request-log";
|
|
67
73
|
import {
|
|
68
74
|
upstreamHostHealthKey,
|
|
69
75
|
normalizeUpstreamHostCircuitThreshold,
|
|
@@ -87,11 +93,15 @@ import {
|
|
|
87
93
|
usesCodexForwardPoolAuth,
|
|
88
94
|
codexWsQuotaObserver,
|
|
89
95
|
isFixedCodexAccount,
|
|
90
|
-
|
|
96
|
+
codexPoolAccountModel400Denial,
|
|
91
97
|
shouldRetryCodexPoolAccountQuota,
|
|
92
98
|
shouldRetryCodexPoolAccountTransient,
|
|
93
99
|
retryCodexPoolOnAlternateAccount,
|
|
94
100
|
} from "./core-codex-account";
|
|
101
|
+
import {
|
|
102
|
+
clearCodexModelDenialEvidence,
|
|
103
|
+
recordCodexModelDenialEvidence,
|
|
104
|
+
} from "../../codex/model-entitlements";
|
|
95
105
|
import { readCodexWsStage } from "./codex-ws-wire";
|
|
96
106
|
import { linkAbortSignal } from "./core-lifetime";
|
|
97
107
|
import type { CodexAuthContext } from "../../codex/auth-context";
|
|
@@ -101,9 +111,8 @@ import {
|
|
|
101
111
|
SendBudgetExhaustedError,
|
|
102
112
|
fetchWithTransientRetry,
|
|
103
113
|
applyUpstreamRecoveryInit,
|
|
104
|
-
TRANSIENT_RETRY_MAX_ATTEMPTS,
|
|
105
114
|
isNonReplayableResponse,
|
|
106
|
-
|
|
115
|
+
prepareSameTarget429Wait,
|
|
107
116
|
sleepWithAbort,
|
|
108
117
|
} from "../../lib/upstream-retry";
|
|
109
118
|
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
@@ -111,7 +120,11 @@ import { classifyTransportFailureKind, transportErrorCode } from "../../lib/upst
|
|
|
111
120
|
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
112
121
|
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
113
122
|
import type { OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
114
|
-
import {
|
|
123
|
+
import {
|
|
124
|
+
rateLimitRetryPolicyFor,
|
|
125
|
+
rateLimitRetryDelayMs,
|
|
126
|
+
transientRetryPolicyFor,
|
|
127
|
+
} from "../../providers/key-failover";
|
|
115
128
|
import type { AttemptRecoveryKind } from "../../usage/log";
|
|
116
129
|
import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
117
130
|
import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
|
|
@@ -194,6 +207,7 @@ export async function preparePassthroughExchange(
|
|
|
194
207
|
| "reserveCredentialHop"
|
|
195
208
|
| "pendingHopPermit"
|
|
196
209
|
| "workflowRootId"
|
|
210
|
+
| "sendsUsed"
|
|
197
211
|
>,
|
|
198
212
|
) {
|
|
199
213
|
const { config, logCtx, options, req } = requestContext;
|
|
@@ -251,6 +265,19 @@ export async function preparePassthroughExchange(
|
|
|
251
265
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
|
|
252
266
|
rememberResponseState(parsed._rawBody, response, undefined, responseStateOptions(true))
|
|
253
267
|
: undefined;
|
|
268
|
+
if (options.nativeControl && nativeResponseControlEligible(route.provider, options.nativeControl)
|
|
269
|
+
&& options.inboundTransport === "websocket" && !options.comboAttempt) {
|
|
270
|
+
const body = parsed._rawBody as Record<string, unknown>;
|
|
271
|
+
if (options.nativeControl.kind === "steering") {
|
|
272
|
+
options.nativeControl.normalizeContinuation = createSteeringSettingsNormalizer(parsed, route, config, req.headers);
|
|
273
|
+
}
|
|
274
|
+
const Replay = options.nativeControl.kind === "injection" ? NativeInjectionReplay : NativeSteeringReplay;
|
|
275
|
+
options.nativeControl.replayFactory = () => new Replay(body.input, (input, response) => {
|
|
276
|
+
if (passthroughRecordEligible && !isBodyNonPersistable(body)) {
|
|
277
|
+
rememberResponseState({ ...body, input }, response, undefined, responseStateOptions(true));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
254
281
|
if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
255
282
|
console.warn(
|
|
256
283
|
`[responses] previous_response_id ${parsed.previousResponseId} not found in local replay state `
|
|
@@ -648,6 +675,33 @@ export async function preparePassthroughExchange(
|
|
|
648
675
|
linkAbortSignal(upstream, options.abortSignal);
|
|
649
676
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
650
677
|
let upstreamResponse: Response;
|
|
678
|
+
/**
|
|
679
|
+
* This leg's transient-5xx ladder cap, from the provider's own `transientRetryOn5xx`.
|
|
680
|
+
*
|
|
681
|
+
* The lane used to pass `TRANSIENT_RETRY_MAX_ATTEMPTS` at every call site, so an operator who
|
|
682
|
+
* configured the option on a key-auth `openai-responses` provider changed nothing in either
|
|
683
|
+
* direction, while the same provider on `openai-chat` was tuned normally. That asymmetry is
|
|
684
|
+
* #4893. The gate in `transientRetryPolicyFor` returns null for OAuth and forward providers,
|
|
685
|
+
* so the ChatGPT pool keeps exactly the ladder it has always had.
|
|
686
|
+
*
|
|
687
|
+
* Read per call rather than captured once, for two reasons. `route.provider` is reassigned
|
|
688
|
+
* inside the recovery loop by credential rotation and transport resolution, so a hoisted
|
|
689
|
+
* policy could outlive the provider row it came from. And the configured value is a total for
|
|
690
|
+
* the whole request, so it has to be measured against what the request has already sent at
|
|
691
|
+
* the moment each leg asks.
|
|
692
|
+
*
|
|
693
|
+
* Still bounded by the request: every site feeds this to `remainingTransientSendBudget` or
|
|
694
|
+
* `recoverySendAllowance`, which intersect it with the request-wide base allowance. So a
|
|
695
|
+
* provider can narrow this request's sends exactly and cannot widen the bound that exists to
|
|
696
|
+
* stop per-request amplification (#4546).
|
|
697
|
+
*/
|
|
698
|
+
const transientSendPolicy = () => transientRetryPolicyFor(route.provider);
|
|
699
|
+
const transientSendAttempts = (): number => transientSendCapFor(
|
|
700
|
+
transientSendPolicy()?.attempts,
|
|
701
|
+
sendBudgetState.sendsUsed,
|
|
702
|
+
);
|
|
703
|
+
const configuredTransientSendBudgetExhausted = (): boolean =>
|
|
704
|
+
transientSendPolicy() !== null && transientSendAttempts() === 0;
|
|
651
705
|
/**
|
|
652
706
|
* Refuse a built body that exceeds the operator's configured ceiling, before it is sent.
|
|
653
707
|
*
|
|
@@ -772,6 +826,9 @@ export async function preparePassthroughExchange(
|
|
|
772
826
|
body: request.body,
|
|
773
827
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
774
828
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
829
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
830
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
831
|
+
? options.nativeControl : undefined,
|
|
775
832
|
dispatchOverride: oauthDispatch(request),
|
|
776
833
|
providerName: route.providerName,
|
|
777
834
|
modelId: route.modelId,
|
|
@@ -784,7 +841,7 @@ export async function preparePassthroughExchange(
|
|
|
784
841
|
// retry wrapper replaces — proves the host was reached (#914 review).
|
|
785
842
|
.then(adoptObservedResponse);
|
|
786
843
|
},
|
|
787
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(
|
|
844
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends },
|
|
788
845
|
);
|
|
789
846
|
} catch (err) {
|
|
790
847
|
return transportFailureResponse(err);
|
|
@@ -844,14 +901,15 @@ export async function preparePassthroughExchange(
|
|
|
844
901
|
recordAttemptCredentialSource(logCtx.activeAttempt, route.providerName, route.provider, retryAdapter.name);
|
|
845
902
|
const rebuiltBodyRefusal = refuseOversizedOutboundBody(request);
|
|
846
903
|
if (rebuiltBodyRefusal) return { failed: rebuiltBodyRefusal };
|
|
847
|
-
// The base allowance is spent first
|
|
848
|
-
//
|
|
849
|
-
//
|
|
850
|
-
// outside the try so the finally can hand it back if the leg never
|
|
904
|
+
// The base allowance is spent first. An unconfigured provider may then draw the one shared
|
|
905
|
+
// final-recovery reserve, which keeps a validated sanitized rebuild after a 5xx streak alive
|
|
906
|
+
// at four total sends instead of dying at three. An explicit provider total cannot widen.
|
|
907
|
+
// Reserve outside the try so the finally can hand it back if the leg never reaches its send.
|
|
851
908
|
const allowance = recoverySendAllowance(
|
|
852
|
-
|
|
909
|
+
transientSendAttempts(),
|
|
853
910
|
recoveryClassFor(recovery),
|
|
854
911
|
`${route.providerName}|${route.modelId}|${recovery}`,
|
|
912
|
+
{ allowFinalRecoveryReserve: transientSendPolicy() === null },
|
|
855
913
|
);
|
|
856
914
|
try {
|
|
857
915
|
return await fetchWithTransientRetry(
|
|
@@ -868,6 +926,9 @@ export async function preparePassthroughExchange(
|
|
|
868
926
|
body: request.body,
|
|
869
927
|
}, innerRecovery), upstream.signal, connectMs, parsed.stream,
|
|
870
928
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
929
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
930
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
931
|
+
? options.nativeControl : undefined,
|
|
871
932
|
dispatchOverride: oauthDispatch(request),
|
|
872
933
|
providerName: route.providerName,
|
|
873
934
|
modelId: route.modelId,
|
|
@@ -974,6 +1035,9 @@ export async function preparePassthroughExchange(
|
|
|
974
1035
|
// here on is a genuine transport attempt.
|
|
975
1036
|
storedPoolReplayDispatchNotifier(
|
|
976
1037
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1038
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1039
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1040
|
+
? options.nativeControl : undefined,
|
|
977
1041
|
dispatchOverride: oauthDispatch(request),
|
|
978
1042
|
providerName: route.providerName,
|
|
979
1043
|
modelId: route.modelId,
|
|
@@ -1007,7 +1071,7 @@ export async function preparePassthroughExchange(
|
|
|
1007
1071
|
// Refused here, before the 401 body is cancelled: once it is gone the request can only
|
|
1008
1072
|
// answer with a synthetic 502, which would report a proxy budget decision as an upstream
|
|
1009
1073
|
// fault and throw away the credential evidence the client needs.
|
|
1010
|
-
&& !sendBudgetExhausted()
|
|
1074
|
+
&& !sendBudgetExhausted(transientSendAttempts())
|
|
1011
1075
|
) {
|
|
1012
1076
|
oauth401ReplayAttempted = true;
|
|
1013
1077
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
@@ -1095,6 +1159,9 @@ export async function preparePassthroughExchange(
|
|
|
1095
1159
|
body: request.body,
|
|
1096
1160
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
1097
1161
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1162
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1163
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1164
|
+
? options.nativeControl : undefined,
|
|
1098
1165
|
dispatchOverride: oauthDispatch(request),
|
|
1099
1166
|
providerName: route.providerName,
|
|
1100
1167
|
modelId: route.modelId,
|
|
@@ -1105,7 +1172,7 @@ export async function preparePassthroughExchange(
|
|
|
1105
1172
|
route.provider.authMode === "forward")
|
|
1106
1173
|
.then(adoptObservedResponse);
|
|
1107
1174
|
},
|
|
1108
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(
|
|
1175
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends },
|
|
1109
1176
|
);
|
|
1110
1177
|
} catch (err) {
|
|
1111
1178
|
return transportFailureResponse(err);
|
|
@@ -1116,13 +1183,13 @@ export async function preparePassthroughExchange(
|
|
|
1116
1183
|
|
|
1117
1184
|
// Native Responses returns before the generic adapter's OAuth rotation loop. Keep
|
|
1118
1185
|
// the same quorum, cooldown and request budget here, before any client bytes flow.
|
|
1119
|
-
|
|
1120
|
-
|
|
1186
|
+
if (
|
|
1187
|
+
upstreamResponse.status === 429
|
|
1121
1188
|
// Not a provider rate limit when this proxy synthesized it for a refused reset
|
|
1122
1189
|
// replay; rotating accounts on it would re-send an inference that may already
|
|
1123
1190
|
// have run and would cool down an account that refused nothing.
|
|
1124
1191
|
&& !isNonReplayableResponse(upstreamResponse)
|
|
1125
|
-
|
|
1192
|
+
&& transportState.genericFailoverAccountId
|
|
1126
1193
|
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
1127
1194
|
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
1128
1195
|
) {
|
|
@@ -1138,6 +1205,8 @@ export async function preparePassthroughExchange(
|
|
|
1138
1205
|
const nextAccountId = rotateGenericOAuthAccountOn429(
|
|
1139
1206
|
config, route.providerName, transportState.genericFailoverAccountId,
|
|
1140
1207
|
upstreamResponse.headers.get("retry-after"),
|
|
1208
|
+
Date.now(),
|
|
1209
|
+
route.modelId,
|
|
1141
1210
|
);
|
|
1142
1211
|
let snapshot: OAuthAccessSnapshot | undefined;
|
|
1143
1212
|
if (nextAccountId) {
|
|
@@ -1165,6 +1234,11 @@ export async function preparePassthroughExchange(
|
|
|
1165
1234
|
}
|
|
1166
1235
|
// No credential moved, so the reservation costs nothing.
|
|
1167
1236
|
hop.permit?.release();
|
|
1237
|
+
} else {
|
|
1238
|
+
// Rotation was available -- the roster cap above admitted it -- and the shared request
|
|
1239
|
+
// budget refused. Recorded so a one-send log is not read as "nothing was eligible",
|
|
1240
|
+
// which is the ambiguity this attribution exists to remove (#5044).
|
|
1241
|
+
noteAttemptRecoveryWithheld(logCtx.activeAttempt, "rotation-send-budget");
|
|
1168
1242
|
}
|
|
1169
1243
|
}
|
|
1170
1244
|
|
|
@@ -1174,15 +1248,15 @@ export async function preparePassthroughExchange(
|
|
|
1174
1248
|
// immediately with no same-key replay. Pre-stream only — nothing has been relayed yet, so
|
|
1175
1249
|
// the replay is lossless (same invariant as the recovery loop). Forward/OAuth providers
|
|
1176
1250
|
// keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
|
|
1177
|
-
|
|
1178
|
-
|
|
1251
|
+
while (
|
|
1252
|
+
upstreamResponse.status === 429
|
|
1179
1253
|
&& !isNonReplayableResponse(upstreamResponse)
|
|
1180
|
-
|
|
1254
|
+
&& rateLimitPolicy !== null
|
|
1181
1255
|
&& rateLimitRetries < rateLimitPolicy.attempts
|
|
1182
1256
|
// Checked here rather than inside the helper: prepareSameTarget429Wait releases the 429
|
|
1183
1257
|
// body, so a refusal discovered after the wait can no longer return the real rate-limit
|
|
1184
1258
|
// answer and would surface a synthetic 502 instead.
|
|
1185
|
-
&& !sendBudgetExhausted()
|
|
1259
|
+
&& !sendBudgetExhausted(transientSendAttempts())
|
|
1186
1260
|
) {
|
|
1187
1261
|
rateLimitRetries += 1;
|
|
1188
1262
|
// Release unread body + deliberate wait via the shared same-target helper.
|
|
@@ -1217,6 +1291,9 @@ export async function preparePassthroughExchange(
|
|
|
1217
1291
|
body: request.body,
|
|
1218
1292
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
1219
1293
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1294
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1295
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1296
|
+
? options.nativeControl : undefined,
|
|
1220
1297
|
dispatchOverride: oauthDispatch(request),
|
|
1221
1298
|
providerName: route.providerName,
|
|
1222
1299
|
modelId: route.modelId,
|
|
@@ -1227,7 +1304,7 @@ export async function preparePassthroughExchange(
|
|
|
1227
1304
|
route.provider.authMode === "forward")
|
|
1228
1305
|
.then(adoptObservedResponse);
|
|
1229
1306
|
},
|
|
1230
|
-
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(
|
|
1307
|
+
{ abortSignal: upstream.signal, label: safeHostLabel(request.url), attempts: remainingTransientSendBudget(transientSendAttempts()), onSendsConsumed: noteTransientSends },
|
|
1231
1308
|
);
|
|
1232
1309
|
} catch (err) {
|
|
1233
1310
|
return transportFailureResponse(err);
|
|
@@ -1259,11 +1336,25 @@ export async function preparePassthroughExchange(
|
|
|
1259
1336
|
|
|
1260
1337
|
if (usesCodexForwardPoolAuth(admissionState.authCtx, route.provider)) {
|
|
1261
1338
|
let poolRetryOutcome: number | undefined;
|
|
1262
|
-
|
|
1339
|
+
// A success is the freshest evidence there is about this pair, and it outranks any earlier
|
|
1340
|
+
// refusal: whatever the entitlement was when upstream declined, it is not that now. Both
|
|
1341
|
+
// ids are cleared because the wire model can differ from the routed one.
|
|
1342
|
+
if (upstreamResponse.ok) {
|
|
1343
|
+
clearCodexModelDenialEvidence(admissionState.authCtx.accountId, route.modelId);
|
|
1344
|
+
clearCodexModelDenialEvidence(admissionState.authCtx.accountId, parsed.modelId);
|
|
1345
|
+
}
|
|
1346
|
+
const model400Denial = await codexPoolAccountModel400Denial(
|
|
1263
1347
|
upstreamResponse,
|
|
1264
1348
|
route.modelId,
|
|
1265
1349
|
options.abortSignal,
|
|
1266
|
-
|
|
1350
|
+
parsed.modelId,
|
|
1351
|
+
);
|
|
1352
|
+
if (model400Denial !== undefined) {
|
|
1353
|
+
// Spend this refusal on more than one retry. It is the account's own authenticated
|
|
1354
|
+
// answer about this model, and the roster cache that selection otherwise reads expires
|
|
1355
|
+
// five minutes after a catalog sync fills it -- so without remembering this, the next
|
|
1356
|
+
// request selects the same account on quota alone and takes the same 400 (#4906).
|
|
1357
|
+
recordCodexModelDenialEvidence(admissionState.authCtx.accountId, model400Denial);
|
|
1267
1358
|
poolRetryOutcome = 400;
|
|
1268
1359
|
} else if (!admissionState.authCtx.fixedAccount && await shouldRetryCodexPoolAccountQuota(
|
|
1269
1360
|
upstreamResponse,
|
|
@@ -1333,18 +1424,20 @@ export async function preparePassthroughExchange(
|
|
|
1333
1424
|
// eviction, or an older transcript). Inspect only a bounded clone of a 4xx whose exact outbound
|
|
1334
1425
|
// Responses body still carries opaque state, then rebuild once through the ordinary adapter
|
|
1335
1426
|
// sanitation path. A second rejection falls through unchanged because the guard stays armed.
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1427
|
+
if (!configuredTransientSendBudgetExhausted()) {
|
|
1428
|
+
const opaqueBlobRecovery = await attemptOpaqueBlobRecovery({
|
|
1429
|
+
response: upstreamResponse,
|
|
1430
|
+
outboundBody: request.body,
|
|
1431
|
+
adapterName: transportState.adapter.name,
|
|
1432
|
+
parsed,
|
|
1433
|
+
guard: opaqueBlobRecoveryGuard,
|
|
1434
|
+
signal: upstream.signal,
|
|
1435
|
+
}, rebuildAndRefetch);
|
|
1436
|
+
if (opaqueBlobRecovery.kind === "failed") return opaqueBlobRecovery.response;
|
|
1437
|
+
if (opaqueBlobRecovery.kind === "recovered") {
|
|
1438
|
+
upstreamResponse = opaqueBlobRecovery.response;
|
|
1439
|
+
continue passthroughRecovery;
|
|
1440
|
+
}
|
|
1348
1441
|
}
|
|
1349
1442
|
|
|
1350
1443
|
const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -1352,6 +1445,7 @@ export async function preparePassthroughExchange(
|
|
|
1352
1445
|
&& !!upstreamResponse.body
|
|
1353
1446
|
&& (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && parsed.stream))
|
|
1354
1447
|
&& !opaqueBlobRecoveryGuard.attempted
|
|
1448
|
+
&& !configuredTransientSendBudgetExhausted()
|
|
1355
1449
|
&& outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
|
|
1356
1450
|
if (streamedFunctionOutputCandidate) {
|
|
1357
1451
|
const preflightLog: RequestLogContext = { model: logCtx.model, provider: logCtx.provider };
|
|
@@ -1368,19 +1462,21 @@ export async function preparePassthroughExchange(
|
|
|
1368
1462
|
if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
|
|
1369
1463
|
upstreamResponse = preflight.response;
|
|
1370
1464
|
if (preflight.kind === "failed") {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1465
|
+
if (!configuredTransientSendBudgetExhausted()) {
|
|
1466
|
+
const streamedOpaqueRecovery = await attemptOpaqueBlobRecovery({
|
|
1467
|
+
response: upstreamResponse,
|
|
1468
|
+
outboundBody: request.body,
|
|
1469
|
+
adapterName: transportState.adapter.name,
|
|
1470
|
+
parsed,
|
|
1471
|
+
guard: opaqueBlobRecoveryGuard,
|
|
1472
|
+
signal: upstream.signal,
|
|
1473
|
+
}, rebuildAndRefetch);
|
|
1474
|
+
if (streamedOpaqueRecovery.kind === "failed") return streamedOpaqueRecovery.response;
|
|
1475
|
+
if (streamedOpaqueRecovery.kind === "recovered") {
|
|
1476
|
+
resetStreamedOpaqueBlobLogContext(logCtx);
|
|
1477
|
+
upstreamResponse = streamedOpaqueRecovery.response;
|
|
1478
|
+
continue passthroughRecovery;
|
|
1479
|
+
}
|
|
1384
1480
|
}
|
|
1385
1481
|
logCtx.upstreamError = preflightLog.upstreamError;
|
|
1386
1482
|
logCtx.terminalHttpStatus = preflightLog.terminalHttpStatus;
|
|
@@ -1399,6 +1495,7 @@ export async function preparePassthroughExchange(
|
|
|
1399
1495
|
upstream.signal,
|
|
1400
1496
|
);
|
|
1401
1497
|
if (uploadRejectionBody !== undefined
|
|
1498
|
+
&& !configuredTransientSendBudgetExhausted()
|
|
1402
1499
|
&& isTransientConsoleGoUploadRejection({
|
|
1403
1500
|
status: upstreamResponse.status,
|
|
1404
1501
|
errorBody: uploadRejectionBody,
|
|
@@ -1437,7 +1534,7 @@ export async function preparePassthroughExchange(
|
|
|
1437
1534
|
requested: parsed.options.reasoning,
|
|
1438
1535
|
rejectionText,
|
|
1439
1536
|
});
|
|
1440
|
-
if (downgrade) {
|
|
1537
|
+
if (downgrade && !configuredTransientSendBudgetExhausted()) {
|
|
1441
1538
|
reasoningEffortDowngradeGuard.attempted = true;
|
|
1442
1539
|
parsed.options.reasoning = downgrade.effort;
|
|
1443
1540
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
@@ -10,6 +10,8 @@ import type { ResponsesEffects } from "./response-effects";
|
|
|
10
10
|
import type { ResponsesSendBudget } from "./request-send-budget";
|
|
11
11
|
import { preparePassthroughExchange } from "./passthrough-dispatch";
|
|
12
12
|
import { deliverPassthroughResponse } from "./passthrough-delivery";
|
|
13
|
+
import { guardDirectPassthroughBodyInactivity } from "../../lib/response-body-inactivity";
|
|
14
|
+
import { resolveStallTimeoutSec } from "../../stall-timeout";
|
|
13
15
|
import { releaseUpstreamHostAdmission } from "../../codex/upstream-host-health";
|
|
14
16
|
import { releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
|
|
15
17
|
|
|
@@ -36,7 +38,7 @@ export async function executePassthroughResponse(
|
|
|
36
38
|
sendBudgetState,
|
|
37
39
|
);
|
|
38
40
|
if (nativeExchange instanceof Response) return nativeExchange;
|
|
39
|
-
|
|
41
|
+
const response = await deliverPassthroughResponse(
|
|
40
42
|
requestContext,
|
|
41
43
|
admissionState,
|
|
42
44
|
requestState,
|
|
@@ -45,6 +47,14 @@ export async function executePassthroughResponse(
|
|
|
45
47
|
responseEffects,
|
|
46
48
|
nativeExchange,
|
|
47
49
|
);
|
|
50
|
+
// Delivery has already classified SSE (including a missing upstream content type)
|
|
51
|
+
// and consumed bounded JSON/errors. Guard only its remaining direct body, outside
|
|
52
|
+
// the relay/lifetime wrappers so their prefetch cannot arm our inactivity clock.
|
|
53
|
+
return guardDirectPassthroughBodyInactivity(
|
|
54
|
+
response,
|
|
55
|
+
nativeExchange.upstream.signal,
|
|
56
|
+
resolveStallTimeoutSec(requestContext.config.stallTimeoutSec) * 1000,
|
|
57
|
+
);
|
|
48
58
|
} finally {
|
|
49
59
|
if (nativeHostState.lease) {
|
|
50
60
|
releaseUpstreamHostAdmission(nativeHostState.lease);
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
previewCodexPoolLineage,
|
|
37
37
|
applyCodexAuthContextToProvider,
|
|
38
38
|
hasCallerCodexBearer,
|
|
39
|
+
requestOwnedMainPinState,
|
|
39
40
|
} from "../../codex/auth-context";
|
|
40
41
|
import {
|
|
41
42
|
copyPreviousResponseReplayProvenance,
|
|
@@ -104,7 +105,9 @@ import { hasUnmappedRoutedCustomToolOutput } from "../../responses/custom-tool-c
|
|
|
104
105
|
import { PROVIDER_OWNED_CONTINUATION_WIRES, resolvedAdapterWire } from "../../responses/continuation-ownership";
|
|
105
106
|
import {
|
|
106
107
|
isCodexReserveHelperUnsupported,
|
|
108
|
+
isCodexReserveOptInMissing,
|
|
107
109
|
CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE,
|
|
110
|
+
CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE,
|
|
108
111
|
} from "../../codex/loopback-target";
|
|
109
112
|
import { checkComboTargetInputAdmission, checkInputAdmission } from "./input-admission";
|
|
110
113
|
import { nativeContextLimits } from "../../codex/catalog";
|
|
@@ -230,6 +233,10 @@ export async function prepareResponsesRequest(
|
|
|
230
233
|
(body as { input?: unknown } | undefined)?.input,
|
|
231
234
|
);
|
|
232
235
|
const inboundClientThreadId = req.headers.get("x-codex-parent-thread-id")?.trim() || undefined;
|
|
236
|
+
// The request's OWN thread, which `x-codex-parent-thread-id` is not: parallel children of one
|
|
237
|
+
// parent all present the same parent id. `codexConversationIdentity` already reads this header
|
|
238
|
+
// for the same reason, and a surface that must tell siblings apart needs it too (#5033).
|
|
239
|
+
const inboundOwnThreadId = req.headers.get("thread-id")?.trim() || undefined;
|
|
233
240
|
const cursorClientThreadId = codexPoolAffinityKey(req.headers);
|
|
234
241
|
const originalBody = body;
|
|
235
242
|
if (options.comboReplaySnapshot) {
|
|
@@ -302,6 +309,7 @@ export async function prepareResponsesRequest(
|
|
|
302
309
|
? options.comboReplaySnapshot.providerContinuation
|
|
303
310
|
: previousResponseProviderState(parsed.previousResponseId);
|
|
304
311
|
if (providerContinuationCandidate) parsed._providerContinuationCandidate = providerContinuationCandidate;
|
|
312
|
+
if (inboundOwnThreadId) parsed._codexOwnThreadId = inboundOwnThreadId;
|
|
305
313
|
if (inboundClientThreadId) {
|
|
306
314
|
parsed._clientThreadId = inboundClientThreadId;
|
|
307
315
|
} else if (
|
|
@@ -483,11 +491,37 @@ export async function prepareResponsesRequest(
|
|
|
483
491
|
const nativeMainReadsForbidden = previewRequestScopedMainCredential
|
|
484
492
|
|| nativeMainRecoveryBlocked
|
|
485
493
|
|| previewSelectionAdmission?.mainProfileDraining === true;
|
|
494
|
+
// The liveness answer final authentication gives its own selection options, computed from the
|
|
495
|
+
// same shared predicate so the two cannot drift apart again (#4850). `fixedAccountId` is
|
|
496
|
+
// mirrored through `route.codexAccountId` because that is literally what core-auth.ts passes
|
|
497
|
+
// as `accountId`. A reserve-authorized request is the one input where the two can differ, and
|
|
498
|
+
// it differs harmlessly: reserve plus a caller bearer is served as main either way, which is
|
|
499
|
+
// the answer this produces.
|
|
500
|
+
const previewRequestOwnedMainPin = requestOwnedMainPinState(
|
|
501
|
+
previewAuthHeaders,
|
|
502
|
+
config,
|
|
503
|
+
options.codexAuthPolicy ?? config,
|
|
504
|
+
previewRequestScopedMainCredential,
|
|
505
|
+
route.codexAccountId,
|
|
506
|
+
).preserve;
|
|
486
507
|
// Deliberately NOT fenced on ownership: final auth derives `nativeMainSelectionOnly` from the
|
|
487
508
|
// drain alone, and adding a term here would diverge from it in the other direction.
|
|
488
509
|
const previewSelectionOptions = {
|
|
489
510
|
nativeMainSelectionOnly: !nativeMainRecoveryBlocked
|
|
490
511
|
&& previewSelectionAdmission?.mainProfileDraining === true,
|
|
512
|
+
// Pool eligibility was the last part of preview still outside the fence (#4850). Without
|
|
513
|
+
// this seam `codexAccountUnusableReason` takes its default branch into
|
|
514
|
+
// `isMainAccountCredentialUsable()`, which opens the physical `auth.json` -- twice per
|
|
515
|
+
// spawn, because subagent fallback re-enters the preview through the callback below.
|
|
516
|
+
//
|
|
517
|
+
// Scoped to ownership, and carrying final auth's value rather than a constant, because
|
|
518
|
+
// preview exists to predict final auth. Under an effective main pin the request really is
|
|
519
|
+
// served by its own main credential, so main must stay eligible; without the pin final auth
|
|
520
|
+
// scores main `main_credential_unavailable` and drops it, so preview has to drop it too. A
|
|
521
|
+
// hardcoded `true` would be wrong in the second case and `false` in the first.
|
|
522
|
+
isMainAccountTokenLive: previewRequestScopedMainCredential
|
|
523
|
+
? () => previewRequestOwnedMainPin
|
|
524
|
+
: undefined,
|
|
491
525
|
// Preview must reach the same answer as the final resolution, including the uploaded-file
|
|
492
526
|
// retention (#4778): a preview that reported a quota move the request will not make would
|
|
493
527
|
// hand subagent fallback a different account than the one that actually serves.
|
|
@@ -672,6 +706,7 @@ export async function prepareResponsesRequest(
|
|
|
672
706
|
"_providerContinuationOwner",
|
|
673
707
|
"_cursorConversationId",
|
|
674
708
|
"_clientThreadId",
|
|
709
|
+
"_codexOwnThreadId",
|
|
675
710
|
"_promptCacheKeyIsSharedCohort",
|
|
676
711
|
"_cursorClientThreadId",
|
|
677
712
|
"_reasoningReplayScope",
|
|
@@ -711,9 +746,23 @@ export async function prepareResponsesRequest(
|
|
|
711
746
|
route,
|
|
712
747
|
options,
|
|
713
748
|
).requestScopedMainCredential && hasCallerCodexBearer(recoveryAuthHeaders);
|
|
749
|
+
// Recovery's own answer to the same question, against the route it may have moved
|
|
750
|
+
// to. Reconstructing the options without it is what left pool eligibility outside
|
|
751
|
+
// the fence on the first preview (#4850); recovery re-previews, so it would leave
|
|
752
|
+
// the same two reads on the one path that runs after decryption.
|
|
753
|
+
const recoveryRequestOwnedMainPin = requestOwnedMainPinState(
|
|
754
|
+
recoveryAuthHeaders,
|
|
755
|
+
config,
|
|
756
|
+
options.codexAuthPolicy ?? config,
|
|
757
|
+
recoveryRequestScopedMainCredential,
|
|
758
|
+
route.codexAccountId,
|
|
759
|
+
).preserve;
|
|
714
760
|
const recoverySelectionOptions = {
|
|
715
761
|
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
716
762
|
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
763
|
+
isMainAccountTokenLive: recoveryRequestScopedMainCredential
|
|
764
|
+
? () => recoveryRequestOwnedMainPin
|
|
765
|
+
: undefined,
|
|
717
766
|
// #4778, same reason as `previewSelectionOptions` above: this preview decides
|
|
718
767
|
// which account subagent fallback scores against, and final auth passes the
|
|
719
768
|
// retention. Recovery is exactly where the two could diverge -- it re-previews
|
|
@@ -935,6 +984,27 @@ export async function prepareResponsesRequest(
|
|
|
935
984
|
options.admission, options.visionDescribeTerminal === true)) {
|
|
936
985
|
return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE);
|
|
937
986
|
}
|
|
987
|
+
// #4940: the opt-in is off, so every Reserve affordance in this process is inert -- no catalog
|
|
988
|
+
// row, no main-credential substitution, no authorization handshake, and no `luna-reserve` header
|
|
989
|
+
// on the send. Forwarding `gpt-reserve` as an ordinary native model therefore buys nothing but a
|
|
990
|
+
// 429 "The usage limit has been reached", which names neither the real cause nor the setting the
|
|
991
|
+
// operator would have to change. Refuse here instead, on the same terms and in the same place as
|
|
992
|
+
// the helper refusal above: after alias/combo resolution, before auth, host-circuit budget or any
|
|
993
|
+
// upstream byte.
|
|
994
|
+
//
|
|
995
|
+
// Two narrowings beyond the predicate, both about not answering a question this refusal cannot
|
|
996
|
+
// answer correctly. A terminal vision/search helper is excluded because enabling the opt-in would
|
|
997
|
+
// not make it work -- it would produce the helper refusal above instead, so telling that caller to
|
|
998
|
+
// enable the flag is advice that does not hold. Non-native inbound wires are excluded because a
|
|
999
|
+
// `gpt-reserve` selector reaching us over Chat or Anthropic Messages is an operator-authored
|
|
1000
|
+
// route (a `claudeCode.modelMap` entry, say), not a Codex client that was forced onto Reserve by
|
|
1001
|
+
// its own usage snapshot, and that route keeps whatever behavior it has today.
|
|
1002
|
+
if (inboundWire === "responses"
|
|
1003
|
+
&& options.visionDescribeTerminal !== true
|
|
1004
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)
|
|
1005
|
+
&& isCodexReserveOptInMissing(options.codexAuthPolicy ?? config, route.modelId, options.admission)) {
|
|
1006
|
+
return formatErrorResponse(400, "invalid_request_error", CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE);
|
|
1007
|
+
}
|
|
938
1008
|
// Refuse an input that cannot plausibly fit the model context window before spending auth,
|
|
939
1009
|
// circuit budget, or upstream bandwidth on a turn the provider will reject anyway (#1412).
|
|
940
1010
|
//
|