@bitkyc08/opencodex 2.56.0 → 2.58.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/bin/ocx.mjs +10 -0
- package/gui/dist/assets/{index-D4zuyIxQ.js → index-BbrHOIY0.js} +21 -21
- package/gui/dist/assets/{index-BBOZWGB6.css → index-C5-RdDmD.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +4 -4
- package/src/adapters/codebuddy/adapter.ts +2 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +249 -0
- package/src/adapters/command-code.ts +12 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- package/src/adapters/cursor/envelope-echo.ts +8 -2
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +19 -1
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +55 -5
- package/src/adapters/google-http.ts +38 -13
- package/src/adapters/google.ts +7 -7
- package/src/adapters/kiro/payload.ts +17 -3
- package/src/adapters/kiro/reasoning.ts +70 -7
- package/src/adapters/kiro/stream.ts +8 -2
- package/src/adapters/kiro/wire.ts +2 -1
- package/src/adapters/kiro-events.ts +21 -13
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/tool-name-registry.ts +166 -0
- package/src/adapters/openai-chat/tool-schema.ts +25 -7
- package/src/adapters/openai-chat.ts +8 -8
- package/src/adapters/openai-responses/passthrough.ts +62 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -0
- package/src/adapters/physical-send.ts +50 -0
- package/src/bridge/errors.ts +26 -2
- package/src/bridge/response-json.ts +8 -2
- package/src/bridge/sse.ts +20 -2
- package/src/claude/desktop-profile.ts +66 -9
- package/src/claude/outbound.ts +32 -4
- package/src/cli/account-main.ts +1 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/combo.ts +10 -1
- package/src/cli/config-command.ts +35 -18
- package/src/cli/dispatch.ts +17 -4
- package/src/cli/index.ts +92 -7
- package/src/cli/registry.ts +2 -1
- package/src/cli/system-command.ts +74 -5
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/clients/config-export.ts +7 -3
- package/src/codex/account-label.ts +14 -3
- package/src/codex/account-store.ts +113 -26
- package/src/codex/account-usability.ts +21 -0
- package/src/codex/auth-api/login-flow.ts +14 -2
- package/src/codex/auth-api/reset-credit-service.ts +11 -2
- package/src/codex/auth-context.ts +199 -15
- package/src/codex/catalog/aggregation.ts +80 -1
- package/src/codex/catalog/model-visibility.ts +1 -0
- package/src/codex/catalog/remote.ts +30 -0
- package/src/codex/catalog/retained-sync.ts +9 -1
- package/src/codex/catalog/routed-gather.ts +38 -1
- package/src/codex/cli-install-provenance.ts +7 -1
- package/src/codex/convergence.ts +7 -2
- package/src/codex/desktop-app/types.ts +11 -2
- package/src/codex/desktop-app/windows.ts +5 -5
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +33 -4
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +231 -32
- package/src/codex/inject.ts +12 -16
- package/src/codex/loopback-target.ts +9 -0
- package/src/codex/model-entitlements.ts +152 -15
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/pool-refresh-backoff.ts +12 -3
- package/src/codex/quota-rejection.ts +104 -15
- package/src/codex/routing/cache-affinity.ts +70 -0
- package/src/codex/routing/cooldown-math.ts +10 -0
- package/src/codex/routing/selection.ts +79 -2
- package/src/codex/routing/thread-affinity.ts +50 -2
- package/src/codex/routing/transient-hold-dispatch.ts +141 -0
- package/src/codex/routing.ts +29 -49
- package/src/codex/warmup.ts +1 -1
- package/src/combos/failover.ts +85 -0
- package/src/combos/request.ts +17 -10
- package/src/combos/types.ts +23 -2
- package/src/config/atomic-write.ts +83 -8
- package/src/config/pending-teardown.ts +31 -0
- package/src/config/schema/config-schema.ts +2 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +272 -180
- package/src/images/loop.ts +1 -1
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/errors.ts +17 -0
- package/src/lib/request-execution-budget.ts +147 -21
- package/src/lib/spend-reservation-ledger.ts +18 -0
- package/src/lib/state-store-registrations.ts +6 -2
- package/src/lib/test-home-guard.ts +85 -1
- package/src/lib/upstream-retry.ts +77 -10
- package/src/lib/windows-elevation.ts +76 -14
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/oauth/index.ts +2 -2
- package/src/oauth/key-providers.ts +2 -2
- package/src/providers/derive.ts +6 -0
- package/src/providers/kiro-models.ts +4 -3
- package/src/providers/label.ts +19 -1
- package/src/providers/model-discovery.ts +35 -7
- package/src/providers/registry/entries-core.ts +18 -0
- package/src/providers/registry/entries-extended.ts +59 -28
- package/src/providers/registry/model-seeds.ts +71 -17
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/reasoning-envelope.ts +6 -3
- 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/routing/identity-domains.ts +21 -14
- package/src/routing/probe-lease.ts +103 -1
- package/src/server/auth-cors.ts +1 -0
- package/src/server/chat-completions.ts +3 -1
- package/src/server/chat-native.ts +37 -9
- package/src/server/index/live-sideband.ts +37 -1
- package/src/server/index/websocket-handler.ts +54 -3
- package/src/server/index.ts +5 -5
- package/src/server/inspection-tee.ts +107 -0
- package/src/server/live.ts +46 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +27 -5
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +14 -19
- package/src/server/request-log.ts +127 -3
- package/src/server/response-log-body.ts +153 -0
- package/src/server/responses/account-change-state.ts +74 -0
- package/src/server/responses/adapter-continuation.ts +33 -7
- package/src/server/responses/adapter-delivery.ts +5 -11
- package/src/server/responses/adapter-dispatch.ts +84 -13
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/codex-ws-wire.ts +5 -0
- package/src/server/responses/collaboration.ts +74 -4
- package/src/server/responses/combo-session-recall.ts +68 -8
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +54 -13
- package/src/server/responses/core-auth.ts +2 -0
- package/src/server/responses/core-codex-account.ts +51 -3
- package/src/server/responses/core-combo.ts +129 -23
- package/src/server/responses/core-errors.ts +18 -0
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/core-replay.ts +105 -32
- package/src/server/responses/core.ts +3 -3
- package/src/server/responses/encrypted-payload.ts +0 -1
- package/src/server/responses/fetch-helpers.ts +4 -1
- package/src/server/responses/input-admission.ts +126 -6
- 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 +30 -6
- package/src/server/responses/passthrough-dispatch.ts +61 -11
- package/src/server/responses/passthrough-error.ts +38 -2
- package/src/server/responses/request-prepare.ts +173 -22
- package/src/server/responses/request-send-budget.ts +97 -2
- package/src/server/responses/request-spend.ts +147 -0
- package/src/server/responses/request-transport.ts +62 -3
- package/src/server/responses/run-turn-execution.ts +59 -31
- package/src/server/responses/sidecar-execution.ts +7 -13
- package/src/server/responses/terminal-guard.ts +65 -4
- package/src/server/responses/ws-upstream.ts +21 -1
- package/src/server/responses-undeclared-tool-guard.ts +9 -5
- package/src/server/stop-teardown.ts +8 -1
- package/src/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +13 -1
- package/src/service/windows-ops.ts +210 -16
- package/src/service/windows-scheduler.ts +28 -21
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -1
- package/src/types/provider.ts +13 -0
- package/src/types/request.ts +8 -5
- package/src/types/tools.ts +24 -0
- package/src/types.ts +2 -0
- package/src/update/index.ts +10 -0
- package/src/update/stop-contract.d.mts +1 -0
- package/src/update/stop-contract.mjs +19 -0
- package/src/update/stop-decision.d.mts +1 -1
- package/src/update/stop-decision.mjs +12 -3
- package/src/usage/log.ts +1 -1
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/vision/describe.ts +5 -5
- package/src/web-search/anthropic-executor.ts +1 -1
- package/src/web-search/exa-executor.ts +1 -1
- package/src/web-search/executor.ts +1 -1
- package/src/web-search/gemini-executor.ts +1 -1
- package/src/web-search/loop.ts +1 -1
- package/src/web-search/ollama-executor.ts +1 -1
- package/src/web-search/parse.ts +67 -14
- package/src/web-search/passthrough-bridge.ts +64 -31
- package/src/web-search/xai-executor.ts +1 -1
|
@@ -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,
|
|
@@ -10,7 +14,7 @@ import type { ResponsesSendBudget } from "./request-send-budget";
|
|
|
10
14
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
11
15
|
import { codexSafetyBufferingFilterOptions, terminalStatusFromParsed } from "../relay";
|
|
12
16
|
import { imageGenToolCallAliases } from "../responses-image-gen-repair";
|
|
13
|
-
import { rememberResponseState } from "../../responses/state";
|
|
17
|
+
import { rememberResponseState, isBodyNonPersistable } from "../../responses/state";
|
|
14
18
|
import {
|
|
15
19
|
currentTurnWireToolCatalogBody,
|
|
16
20
|
hasExplicitWireToolCatalog,
|
|
@@ -29,6 +33,7 @@ import {
|
|
|
29
33
|
unwrapUpstreamRetryEvidenceError,
|
|
30
34
|
codexProbeLeaseId,
|
|
31
35
|
codexProbeQuotaScope,
|
|
36
|
+
codexTransientProbeGrant,
|
|
32
37
|
createCodexReserveDispatchGuard,
|
|
33
38
|
} from "../../codex/auth-context";
|
|
34
39
|
import {
|
|
@@ -60,7 +65,6 @@ import { restorePlaintextV2AgentMessageCalls } from "../../responses/plaintext-v
|
|
|
60
65
|
import {
|
|
61
66
|
recordAdapterReasoning,
|
|
62
67
|
recordAdapterTier,
|
|
63
|
-
noteAttemptSend,
|
|
64
68
|
sealRequestAttemptIdentity,
|
|
65
69
|
recordAttemptCredentialSource,
|
|
66
70
|
} from "../request-log";
|
|
@@ -80,6 +84,7 @@ import {
|
|
|
80
84
|
safeHostLabel,
|
|
81
85
|
storedPoolReplayDispatchNotifier,
|
|
82
86
|
} from "./fetch-helpers";
|
|
87
|
+
import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
|
|
83
88
|
import { clientCancelledResponse } from "./core-errors";
|
|
84
89
|
import {
|
|
85
90
|
upstreamHostCircuitOpenResponse,
|
|
@@ -101,6 +106,7 @@ import {
|
|
|
101
106
|
fetchWithTransientRetry,
|
|
102
107
|
applyUpstreamRecoveryInit,
|
|
103
108
|
TRANSIENT_RETRY_MAX_ATTEMPTS,
|
|
109
|
+
isNonReplayableResponse,
|
|
104
110
|
prepareSameTarget429Wait,
|
|
105
111
|
sleepWithAbort,
|
|
106
112
|
} from "../../lib/upstream-retry";
|
|
@@ -171,6 +177,7 @@ export async function preparePassthroughExchange(
|
|
|
171
177
|
| "replayOAuthCredentialSnapshot"
|
|
172
178
|
| "genericFailovers"
|
|
173
179
|
| "applyFailoverSnapshot"
|
|
180
|
+
| "noteRoutedAttemptSend"
|
|
174
181
|
>,
|
|
175
182
|
responseEffects: Pick<
|
|
176
183
|
ResponsesEffects,
|
|
@@ -248,6 +255,19 @@ export async function preparePassthroughExchange(
|
|
|
248
255
|
? (response: { id?: unknown; output?: unknown; status?: unknown }) =>
|
|
249
256
|
rememberResponseState(parsed._rawBody, response, undefined, responseStateOptions(true))
|
|
250
257
|
: undefined;
|
|
258
|
+
if (options.nativeControl && nativeResponseControlEligible(route.provider, options.nativeControl)
|
|
259
|
+
&& options.inboundTransport === "websocket" && !options.comboAttempt) {
|
|
260
|
+
const body = parsed._rawBody as Record<string, unknown>;
|
|
261
|
+
if (options.nativeControl.kind === "steering") {
|
|
262
|
+
options.nativeControl.normalizeContinuation = createSteeringSettingsNormalizer(parsed, route, config, req.headers);
|
|
263
|
+
}
|
|
264
|
+
const Replay = options.nativeControl.kind === "injection" ? NativeInjectionReplay : NativeSteeringReplay;
|
|
265
|
+
options.nativeControl.replayFactory = () => new Replay(body.input, (input, response) => {
|
|
266
|
+
if (passthroughRecordEligible && !isBodyNonPersistable(body)) {
|
|
267
|
+
rememberResponseState({ ...body, input }, response, undefined, responseStateOptions(true));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
251
271
|
if (parsed.previousResponseId && !parsed._previousResponseInputExpanded) {
|
|
252
272
|
console.warn(
|
|
253
273
|
`[responses] previous_response_id ${parsed.previousResponseId} not found in local replay state `
|
|
@@ -346,10 +366,13 @@ export async function preparePassthroughExchange(
|
|
|
346
366
|
const declaredWireToolNames = new Set<string>();
|
|
347
367
|
const declaredBareWireToolNames = new Set<string>();
|
|
348
368
|
const declaredNamelessClientCallTypes = new Set<string>();
|
|
349
|
-
// `buildToolBridgeMaps`
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
369
|
+
// `buildToolBridgeMaps` adds each eligible bare alias to `declaredToolNames` and `toolNsMap`
|
|
370
|
+
// (one authorized identity claims the bare name). `refreshUndeclaredToolGuard` normally copies
|
|
371
|
+
// those entries into `declaredWireToolNames`, but passthrough restoration runs before the
|
|
372
|
+
// undeclared-tool guard, so restore that request-bounded identity here, before authorization
|
|
373
|
+
// checks. `exec` uses separate handling: its bridge alias is copied into the declared set only
|
|
374
|
+
// when the client itself declared bare `exec`, because otherwise code-mode normalization could
|
|
375
|
+
// authorize the unrelated code-mode helper names.
|
|
353
376
|
const authorizedBareNamespaceToolAliases: RoutedNamespaceToolAliases = new Map(
|
|
354
377
|
[...toolBridgeMaps.toolNsMap].flatMap(([alias, identity]) =>
|
|
355
378
|
alias === identity.name
|
|
@@ -736,6 +759,7 @@ export async function preparePassthroughExchange(
|
|
|
736
759
|
modelId: route.modelId,
|
|
737
760
|
probeLeaseId: codexProbeLeaseId(admissionState.authCtx),
|
|
738
761
|
probeQuotaScope: codexProbeQuotaScope(admissionState.authCtx),
|
|
762
|
+
transientProbe: codexTransientProbeGrant(admissionState.authCtx),
|
|
739
763
|
writerGeneration: admissionState.authCtx.writerGeneration,
|
|
740
764
|
});
|
|
741
765
|
}
|
|
@@ -752,13 +776,22 @@ export async function preparePassthroughExchange(
|
|
|
752
776
|
// Body is a replayable string; nothing has streamed to the client yet.
|
|
753
777
|
upstreamResponse = await fetchWithTransientRetry(
|
|
754
778
|
recovery => {
|
|
755
|
-
|
|
779
|
+
// The pool-wide recovery window measures recovery traffic against observed demand,
|
|
780
|
+
// and this is where demand is observed: `recovery === undefined` is a new request's
|
|
781
|
+
// first send, everything after it is the same request trying again. Without this the
|
|
782
|
+
// ratio has no denominator and the window collapses to its quiet-pool floor, which
|
|
783
|
+
// would throttle recovery on a busy proxy exactly as hard as on an idle one (#4701).
|
|
784
|
+
if (recovery === undefined) classifyPoolRecoveryDispatch("initial");
|
|
785
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, recovery);
|
|
756
786
|
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
757
787
|
method: request.method,
|
|
758
788
|
headers: request.headers,
|
|
759
789
|
body: request.body,
|
|
760
790
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
761
791
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
792
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
793
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
794
|
+
? options.nativeControl : undefined,
|
|
762
795
|
dispatchOverride: oauthDispatch(request),
|
|
763
796
|
providerName: route.providerName,
|
|
764
797
|
modelId: route.modelId,
|
|
@@ -848,13 +881,16 @@ export async function preparePassthroughExchange(
|
|
|
848
881
|
if (allowance.permit && !allowance.permit.use()) {
|
|
849
882
|
throw new SendBudgetExhaustedError(safeHostLabel(request.url));
|
|
850
883
|
}
|
|
851
|
-
|
|
884
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, innerRecovery ?? recovery);
|
|
852
885
|
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
853
886
|
method: request.method,
|
|
854
887
|
headers: request.headers,
|
|
855
888
|
body: request.body,
|
|
856
889
|
}, innerRecovery), upstream.signal, connectMs, parsed.stream,
|
|
857
890
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
891
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
892
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
893
|
+
? options.nativeControl : undefined,
|
|
858
894
|
dispatchOverride: oauthDispatch(request),
|
|
859
895
|
providerName: route.providerName,
|
|
860
896
|
modelId: route.modelId,
|
|
@@ -946,7 +982,7 @@ export async function preparePassthroughExchange(
|
|
|
946
982
|
// every other build site; a replay is exactly when a grown payload reappears.
|
|
947
983
|
const replayBodyRefusal = refuseOversizedOutboundBody(request);
|
|
948
984
|
if (replayBodyRefusal) return replayBodyRefusal;
|
|
949
|
-
|
|
985
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, "oauth-401");
|
|
950
986
|
upstreamResponse = await fetchWithHeaderTimeout(
|
|
951
987
|
request.url,
|
|
952
988
|
{ method: request.method, headers: request.headers, body: request.body },
|
|
@@ -961,6 +997,9 @@ export async function preparePassthroughExchange(
|
|
|
961
997
|
// here on is a genuine transport attempt.
|
|
962
998
|
storedPoolReplayDispatchNotifier(
|
|
963
999
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1000
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1001
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1002
|
+
? options.nativeControl : undefined,
|
|
964
1003
|
dispatchOverride: oauthDispatch(request),
|
|
965
1004
|
providerName: route.providerName,
|
|
966
1005
|
modelId: route.modelId,
|
|
@@ -1075,13 +1114,16 @@ export async function preparePassthroughExchange(
|
|
|
1075
1114
|
try {
|
|
1076
1115
|
upstreamResponse = await fetchWithTransientRetry(
|
|
1077
1116
|
recovery => {
|
|
1078
|
-
|
|
1117
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, recovery ?? "oauth-401");
|
|
1079
1118
|
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
1080
1119
|
method: request.method,
|
|
1081
1120
|
headers: request.headers,
|
|
1082
1121
|
body: request.body,
|
|
1083
1122
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
1084
1123
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1124
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1125
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1126
|
+
? options.nativeControl : undefined,
|
|
1085
1127
|
dispatchOverride: oauthDispatch(request),
|
|
1086
1128
|
providerName: route.providerName,
|
|
1087
1129
|
modelId: route.modelId,
|
|
@@ -1105,6 +1147,10 @@ export async function preparePassthroughExchange(
|
|
|
1105
1147
|
// the same quorum, cooldown and request budget here, before any client bytes flow.
|
|
1106
1148
|
if (
|
|
1107
1149
|
upstreamResponse.status === 429
|
|
1150
|
+
// Not a provider rate limit when this proxy synthesized it for a refused reset
|
|
1151
|
+
// replay; rotating accounts on it would re-send an inference that may already
|
|
1152
|
+
// have run and would cool down an account that refused nothing.
|
|
1153
|
+
&& !isNonReplayableResponse(upstreamResponse)
|
|
1108
1154
|
&& transportState.genericFailoverAccountId
|
|
1109
1155
|
&& transportState.genericFailovers < GENERIC_OAUTH_MAX_FAILOVERS_PER_REQUEST
|
|
1110
1156
|
&& isGenericOAuthFailoverEnabled(config, route.providerName)
|
|
@@ -1159,6 +1205,7 @@ export async function preparePassthroughExchange(
|
|
|
1159
1205
|
// keep their pool logic below (rateLimitRetryPolicyFor returns null for them).
|
|
1160
1206
|
while (
|
|
1161
1207
|
upstreamResponse.status === 429
|
|
1208
|
+
&& !isNonReplayableResponse(upstreamResponse)
|
|
1162
1209
|
&& rateLimitPolicy !== null
|
|
1163
1210
|
&& rateLimitRetries < rateLimitPolicy.attempts
|
|
1164
1211
|
// Checked here rather than inside the helper: prepareSameTarget429Wait releases the 429
|
|
@@ -1192,13 +1239,16 @@ export async function preparePassthroughExchange(
|
|
|
1192
1239
|
recovery => {
|
|
1193
1240
|
// The first send of every replay is itself a rate-limit retry; inner transient-5xx
|
|
1194
1241
|
// recoveries keep their own label (recovery is provided for those).
|
|
1195
|
-
|
|
1242
|
+
transportState.noteRoutedAttemptSend(passthroughEstimate, recovery ?? "rate-limit-429");
|
|
1196
1243
|
return fetchWithHeaderTimeout(request.url, applyUpstreamRecoveryInit({
|
|
1197
1244
|
method: request.method,
|
|
1198
1245
|
headers: request.headers,
|
|
1199
1246
|
body: request.body,
|
|
1200
1247
|
}, recovery), upstream.signal, connectMs, parsed.stream,
|
|
1201
1248
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1249
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1250
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1251
|
+
? options.nativeControl : undefined,
|
|
1202
1252
|
dispatchOverride: oauthDispatch(request),
|
|
1203
1253
|
providerName: route.providerName,
|
|
1204
1254
|
modelId: route.modelId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { formatErrorResponse } from "../../bridge";
|
|
2
2
|
import { isCyberPolicyCode, isCyberPolicyMessage } from "../../lib/errors";
|
|
3
|
+
import { isReplayRefusalCode, UPSTREAM_RESET_REPLAY_REFUSED_CODE } from "../../lib/upstream-retry";
|
|
3
4
|
import {
|
|
4
5
|
resolveClientRetryAfter,
|
|
5
6
|
validateClientRetryAfterHeader,
|
|
@@ -24,6 +25,26 @@ function isCyberPolicyBody(body: string): boolean {
|
|
|
24
25
|
return false;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* True for a body this proxy wrote to refuse replaying an ambiguous pre-header reset.
|
|
30
|
+
*
|
|
31
|
+
* It is read off the body rather than a marker because this formatter is handed bytes, not
|
|
32
|
+
* the response they came from, and the refusal reaches it after the original body was read.
|
|
33
|
+
* The code is this proxy's own, so an upstream echoing it is not a case worth widening for.
|
|
34
|
+
*/
|
|
35
|
+
function isReplayRefusalBody(body: string): boolean {
|
|
36
|
+
if (!body.includes(UPSTREAM_RESET_REPLAY_REFUSED_CODE)) return false;
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
39
|
+
const error = parsed.error && typeof parsed.error === "object" && !Array.isArray(parsed.error)
|
|
40
|
+
? parsed.error as Record<string, unknown>
|
|
41
|
+
: undefined;
|
|
42
|
+
return isReplayRefusalCode(error?.code) || isReplayRefusalCode(parsed.code);
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
/**
|
|
28
49
|
* Passthrough adapters historically relayed upstream non-2xx bodies verbatim.
|
|
29
50
|
* Codex maps an *empty* body to the literal client string "Unknown error"
|
|
@@ -38,6 +59,9 @@ function isCyberPolicyBody(body: string): boolean {
|
|
|
38
59
|
* - missing/malformed values are replaced when resolveClientRetryAfter yields a value
|
|
39
60
|
* - malformed/expired values are removed when the resolver returns undefined
|
|
40
61
|
* (e.g. quota-exhausted 429s must not keep junk headers or get the synthetic "2")
|
|
62
|
+
* - a replay refusal this proxy wrote gets none and keeps none: the whole point of the
|
|
63
|
+
* refusal is that the turn may already be running, and the synthetic default for a
|
|
64
|
+
* retryable 429 is a direct instruction to the client to send it a second time
|
|
41
65
|
*/
|
|
42
66
|
export function formatPassthroughUpstreamError(
|
|
43
67
|
status: number,
|
|
@@ -46,6 +70,13 @@ export function formatPassthroughUpstreamError(
|
|
|
46
70
|
statusText?: string;
|
|
47
71
|
headers?: Headers;
|
|
48
72
|
now?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Provenance from the caller that still holds the response: this body is a refusal this
|
|
75
|
+
* proxy synthesized. The body check below is the fallback for a re-wrapped body, and it
|
|
76
|
+
* cannot answer at all when the bounded read returned nothing display-safe -- which is
|
|
77
|
+
* precisely when the empty-body branch would invent the retryable-429 default.
|
|
78
|
+
*/
|
|
79
|
+
replayRefusal?: boolean;
|
|
49
80
|
},
|
|
50
81
|
): Response {
|
|
51
82
|
const trimmed = bodyText.trim();
|
|
@@ -53,7 +84,12 @@ export function formatPassthroughUpstreamError(
|
|
|
53
84
|
const upstreamRetryAfter = options?.headers?.get("retry-after")?.trim() || undefined;
|
|
54
85
|
const originalValid = validateClientRetryAfterHeader(upstreamRetryAfter, now);
|
|
55
86
|
const cyberPolicyFailure = isCyberPolicyBody(trimmed);
|
|
56
|
-
|
|
87
|
+
// Two different reasons to answer with no wait at all, handled the same way: a hard policy
|
|
88
|
+
// block will not become servable, and a refusal we made was never a rate limit.
|
|
89
|
+
const suppressRetryAfter = cyberPolicyFailure
|
|
90
|
+
|| options?.replayRefusal === true
|
|
91
|
+
|| isReplayRefusalBody(trimmed);
|
|
92
|
+
const resolved = suppressRetryAfter
|
|
57
93
|
? undefined
|
|
58
94
|
: resolveClientRetryAfter({
|
|
59
95
|
status,
|
|
@@ -64,7 +100,7 @@ export function formatPassthroughUpstreamError(
|
|
|
64
100
|
|
|
65
101
|
if (trimmed) {
|
|
66
102
|
const needsSet = resolved !== undefined && upstreamRetryAfter !== resolved;
|
|
67
|
-
const needsDelete = (
|
|
103
|
+
const needsDelete = (suppressRetryAfter && upstreamRetryAfter !== undefined)
|
|
68
104
|
|| (resolved === undefined
|
|
69
105
|
&& upstreamRetryAfter !== undefined
|
|
70
106
|
&& originalValid === undefined);
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
codexPoolAffinityKey,
|
|
36
36
|
previewCodexPoolLineage,
|
|
37
37
|
applyCodexAuthContextToProvider,
|
|
38
|
+
hasCallerCodexBearer,
|
|
39
|
+
requestOwnedMainPinState,
|
|
38
40
|
} from "../../codex/auth-context";
|
|
39
41
|
import {
|
|
40
42
|
copyPreviousResponseReplayProvenance,
|
|
@@ -84,7 +86,11 @@ import {
|
|
|
84
86
|
canPassThroughEncryptedV2AgentTask,
|
|
85
87
|
applyFinalRouteRequestNormalization,
|
|
86
88
|
} from "./core-normalize";
|
|
87
|
-
import {
|
|
89
|
+
import {
|
|
90
|
+
cachedDeniedCodexAccountIdsForModel,
|
|
91
|
+
resolveCodexModelEntitlements,
|
|
92
|
+
} from "../../codex/model-entitlements";
|
|
93
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
88
94
|
import {
|
|
89
95
|
previewCodexAccountForRequest,
|
|
90
96
|
codexQuotaScopeForModel,
|
|
@@ -101,7 +107,7 @@ import {
|
|
|
101
107
|
isCodexReserveHelperUnsupported,
|
|
102
108
|
CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE,
|
|
103
109
|
} from "../../codex/loopback-target";
|
|
104
|
-
import { checkInputAdmission } from "./input-admission";
|
|
110
|
+
import { checkComboTargetInputAdmission, checkInputAdmission } from "./input-admission";
|
|
105
111
|
import { nativeContextLimits } from "../../codex/catalog";
|
|
106
112
|
import { streamingContextOverflowResponse } from "./context-overflow";
|
|
107
113
|
import {
|
|
@@ -115,6 +121,8 @@ import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
|
115
121
|
import {
|
|
116
122
|
conversationStateBindingFromAuth,
|
|
117
123
|
applyAccountChangeConversationStateScrub,
|
|
124
|
+
accountChangeFileReferenceRefusal,
|
|
125
|
+
conversationCarriesUploadedFiles,
|
|
118
126
|
} from "./account-change-state";
|
|
119
127
|
|
|
120
128
|
/** Parses, selects, and admits one request without changing the dispatch policy. */
|
|
@@ -365,6 +373,14 @@ export async function prepareResponsesRequest(
|
|
|
365
373
|
}
|
|
366
374
|
logCtx.requestedModel = parsed.modelId;
|
|
367
375
|
logCtx.requestedEffort = parsed.options.reasoning;
|
|
376
|
+
// What this request may spend beyond its input, for the durable spend reservation (#4707).
|
|
377
|
+
// Read from the caller rather than from the adapter's serialized body, because the
|
|
378
|
+
// reservation has to exist before the body does. A caller that omits it leaves the
|
|
379
|
+
// provider/model default in charge and reserves only the input estimate; settlement then
|
|
380
|
+
// books the real figure, so the gap is a looser bound up front, never a wrong one after.
|
|
381
|
+
if (typeof parsed.options.maxOutputTokens === "number" && parsed.options.maxOutputTokens > 0) {
|
|
382
|
+
logCtx.spendOutputCeilingTokens = Math.trunc(parsed.options.maxOutputTokens);
|
|
383
|
+
}
|
|
368
384
|
logCtx.callerServiceTier = sanitizeLogMetadataString(parsed.options.serviceTier);
|
|
369
385
|
logCtx.requestedServiceTier = parsed.options.serviceTier;
|
|
370
386
|
logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
|
|
@@ -442,12 +458,67 @@ export async function prepareResponsesRequest(
|
|
|
442
458
|
&& (route.codexAccountId === undefined || initialSubagentFallbackChain !== null)
|
|
443
459
|
? codexAccountSelectionForTurn(options.turnAdmissionLease)?.()
|
|
444
460
|
: undefined;
|
|
461
|
+
// The credential headers final authentication will be given, resolved once and reused by
|
|
462
|
+
// everything below that has to predict what final auth decides.
|
|
463
|
+
const previewAuthHeaders = codexRouteCredentialDomainHeaders(
|
|
464
|
+
req,
|
|
465
|
+
route,
|
|
466
|
+
options,
|
|
467
|
+
credentialDomainWasRewritten,
|
|
468
|
+
);
|
|
469
|
+
// Does the CALLER own the credential this request will authenticate with? Validated exactly
|
|
470
|
+
// the way final auth validates it: the route ownership predicate AND the caller-bearer check
|
|
471
|
+
// `resolveCodexAuthContext` re-applies to these same headers.
|
|
472
|
+
const previewRequestScopedMainCredential = codexRouteCredentialOwnership(
|
|
473
|
+
previewAuthHeaders,
|
|
474
|
+
config,
|
|
475
|
+
route,
|
|
476
|
+
options,
|
|
477
|
+
).requestScopedMainCredential && hasCallerCodexBearer(previewAuthHeaders);
|
|
445
478
|
const nativeMainRecoveryBlocked = isNativeMainTrafficBlocked();
|
|
446
|
-
|
|
479
|
+
// The same three inputs final auth ORs together (src/codex/auth-context.ts). Request-owned
|
|
480
|
+
// ownership is first there and has to be first here: computing the preview fence from
|
|
481
|
+
// recovery and drain state alone let a `thread_spawn` carrying a forwardable caller bearer
|
|
482
|
+
// read the physical main token it is forbidden to touch, and score main differently than the
|
|
483
|
+
// resolution this preview exists to predict.
|
|
484
|
+
const nativeMainReadsForbidden = previewRequestScopedMainCredential
|
|
485
|
+
|| nativeMainRecoveryBlocked
|
|
447
486
|
|| previewSelectionAdmission?.mainProfileDraining === true;
|
|
487
|
+
// The liveness answer final authentication gives its own selection options, computed from the
|
|
488
|
+
// same shared predicate so the two cannot drift apart again (#4850). `fixedAccountId` is
|
|
489
|
+
// mirrored through `route.codexAccountId` because that is literally what core-auth.ts passes
|
|
490
|
+
// as `accountId`. A reserve-authorized request is the one input where the two can differ, and
|
|
491
|
+
// it differs harmlessly: reserve plus a caller bearer is served as main either way, which is
|
|
492
|
+
// the answer this produces.
|
|
493
|
+
const previewRequestOwnedMainPin = requestOwnedMainPinState(
|
|
494
|
+
previewAuthHeaders,
|
|
495
|
+
config,
|
|
496
|
+
options.codexAuthPolicy ?? config,
|
|
497
|
+
previewRequestScopedMainCredential,
|
|
498
|
+
route.codexAccountId,
|
|
499
|
+
).preserve;
|
|
500
|
+
// Deliberately NOT fenced on ownership: final auth derives `nativeMainSelectionOnly` from the
|
|
501
|
+
// drain alone, and adding a term here would diverge from it in the other direction.
|
|
448
502
|
const previewSelectionOptions = {
|
|
449
503
|
nativeMainSelectionOnly: !nativeMainRecoveryBlocked
|
|
450
504
|
&& previewSelectionAdmission?.mainProfileDraining === true,
|
|
505
|
+
// Pool eligibility was the last part of preview still outside the fence (#4850). Without
|
|
506
|
+
// this seam `codexAccountUnusableReason` takes its default branch into
|
|
507
|
+
// `isMainAccountCredentialUsable()`, which opens the physical `auth.json` -- twice per
|
|
508
|
+
// spawn, because subagent fallback re-enters the preview through the callback below.
|
|
509
|
+
//
|
|
510
|
+
// Scoped to ownership, and carrying final auth's value rather than a constant, because
|
|
511
|
+
// preview exists to predict final auth. Under an effective main pin the request really is
|
|
512
|
+
// served by its own main credential, so main must stay eligible; without the pin final auth
|
|
513
|
+
// scores main `main_credential_unavailable` and drops it, so preview has to drop it too. A
|
|
514
|
+
// hardcoded `true` would be wrong in the second case and `false` in the first.
|
|
515
|
+
isMainAccountTokenLive: previewRequestScopedMainCredential
|
|
516
|
+
? () => previewRequestOwnedMainPin
|
|
517
|
+
: undefined,
|
|
518
|
+
// Preview must reach the same answer as the final resolution, including the uploaded-file
|
|
519
|
+
// retention (#4778): a preview that reported a quota move the request will not make would
|
|
520
|
+
// hand subagent fallback a different account than the one that actually serves.
|
|
521
|
+
retainAccountForUploadedFiles: conversationCarriesUploadedFiles(parsed._rawBody),
|
|
451
522
|
};
|
|
452
523
|
let selectedForwardHeaders = req.headers;
|
|
453
524
|
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
@@ -466,22 +537,11 @@ export async function prepareResponsesRequest(
|
|
|
466
537
|
// deliberately create no affinity at all -- previewing a family binding for one of those would
|
|
467
538
|
// hand model fallback an account this request can never authenticate as. Read-only: the record
|
|
468
539
|
// is written by the resolution that binds, never by a preview that may own no Pool state.
|
|
469
|
-
const previewAuthHeaders = codexRouteCredentialDomainHeaders(
|
|
470
|
-
req,
|
|
471
|
-
route,
|
|
472
|
-
options,
|
|
473
|
-
credentialDomainWasRewritten,
|
|
474
|
-
);
|
|
475
540
|
const poolLineage = previewCodexPoolLineage(previewAuthHeaders, options.codexAuthPolicy ?? config, {
|
|
476
541
|
accountId: route.codexAccountId,
|
|
477
542
|
modelId: route.modelId,
|
|
478
543
|
admission: options.admission,
|
|
479
|
-
requestScopedMainCredential:
|
|
480
|
-
previewAuthHeaders,
|
|
481
|
-
config,
|
|
482
|
-
route,
|
|
483
|
-
options,
|
|
484
|
-
).requestScopedMainCredential,
|
|
544
|
+
requestScopedMainCredential: previewRequestScopedMainCredential,
|
|
485
545
|
});
|
|
486
546
|
|
|
487
547
|
try {
|
|
@@ -519,7 +579,20 @@ export async function prepareResponsesRequest(
|
|
|
519
579
|
config,
|
|
520
580
|
previewNow,
|
|
521
581
|
codexQuotaScopeForModel(modelId),
|
|
522
|
-
{
|
|
582
|
+
{
|
|
583
|
+
...previewSelectionOptions,
|
|
584
|
+
modelEligibleAccountIds,
|
|
585
|
+
// Per CANDIDATE model, like the scope and the eligible set above: the preference is
|
|
586
|
+
// model-specific, so hoisting it out of the closure would score every fallback
|
|
587
|
+
// candidate against the requested model's evidence and diverge from final auth (#4768).
|
|
588
|
+
// Under the same native-main read fence final auth applies: the reader validates each
|
|
589
|
+
// cached roster against the account's current credential, and for main that is a
|
|
590
|
+
// synchronous read of the stored token. A preview that read it would both cross the
|
|
591
|
+
// fence and score main differently than the resolution it is supposed to predict.
|
|
592
|
+
deniedModelAccountIds: cachedDeniedCodexAccountIdsForModel(modelId, previewNow, {
|
|
593
|
+
excludeAccountIds: nativeMainReadsForbidden ? new Set([MAIN_CODEX_ACCOUNT_ID]) : undefined,
|
|
594
|
+
}),
|
|
595
|
+
},
|
|
523
596
|
modelId,
|
|
524
597
|
poolLineage,
|
|
525
598
|
);
|
|
@@ -650,9 +723,45 @@ export async function prepareResponsesRequest(
|
|
|
650
723
|
const fallback = (() => {
|
|
651
724
|
try {
|
|
652
725
|
const recoveryNativeMainBlocked = isNativeMainTrafficBlocked();
|
|
726
|
+
// Recompute ownership here rather than reusing the pre-decryption value: a
|
|
727
|
+
// subagent fallback above may have re-routed, and `requestScopedMainCredential`
|
|
728
|
+
// is a function of the route as well as the headers.
|
|
729
|
+
const recoveryAuthHeaders = codexRouteCredentialDomainHeaders(
|
|
730
|
+
req,
|
|
731
|
+
route,
|
|
732
|
+
options,
|
|
733
|
+
credentialDomainWasRewritten,
|
|
734
|
+
);
|
|
735
|
+
const recoveryRequestScopedMainCredential = codexRouteCredentialOwnership(
|
|
736
|
+
recoveryAuthHeaders,
|
|
737
|
+
config,
|
|
738
|
+
route,
|
|
739
|
+
options,
|
|
740
|
+
).requestScopedMainCredential && hasCallerCodexBearer(recoveryAuthHeaders);
|
|
741
|
+
// Recovery's own answer to the same question, against the route it may have moved
|
|
742
|
+
// to. Reconstructing the options without it is what left pool eligibility outside
|
|
743
|
+
// the fence on the first preview (#4850); recovery re-previews, so it would leave
|
|
744
|
+
// the same two reads on the one path that runs after decryption.
|
|
745
|
+
const recoveryRequestOwnedMainPin = requestOwnedMainPinState(
|
|
746
|
+
recoveryAuthHeaders,
|
|
747
|
+
config,
|
|
748
|
+
options.codexAuthPolicy ?? config,
|
|
749
|
+
recoveryRequestScopedMainCredential,
|
|
750
|
+
route.codexAccountId,
|
|
751
|
+
).preserve;
|
|
653
752
|
const recoverySelectionOptions = {
|
|
654
753
|
nativeMainSelectionOnly: !recoveryNativeMainBlocked
|
|
655
754
|
&& recoverySelectionAdmission?.mainProfileDraining === true,
|
|
755
|
+
isMainAccountTokenLive: recoveryRequestScopedMainCredential
|
|
756
|
+
? () => recoveryRequestOwnedMainPin
|
|
757
|
+
: undefined,
|
|
758
|
+
// #4778, same reason as `previewSelectionOptions` above: this preview decides
|
|
759
|
+
// which account subagent fallback scores against, and final auth passes the
|
|
760
|
+
// retention. Recovery is exactly where the two could diverge -- it re-previews
|
|
761
|
+
// against the DECRYPTED body, which is the first point at which a file reference
|
|
762
|
+
// that was ciphertext-only becomes readable, so reconstructing the options
|
|
763
|
+
// without the bit lets preview report a quota move the request will not make.
|
|
764
|
+
retainAccountForUploadedFiles: conversationCarriesUploadedFiles(parsed._rawBody),
|
|
656
765
|
};
|
|
657
766
|
const recoveryNow = Date.now();
|
|
658
767
|
// Carry the entitlement filter through recovery too (#2509/#2623). The scope was
|
|
@@ -665,7 +774,21 @@ export async function prepareResponsesRequest(
|
|
|
665
774
|
config,
|
|
666
775
|
previewNow,
|
|
667
776
|
codexQuotaScopeForModel(modelId),
|
|
668
|
-
{
|
|
777
|
+
{
|
|
778
|
+
...recoverySelectionOptions,
|
|
779
|
+
modelEligibleAccountIds,
|
|
780
|
+
// Same read fence as the first preview, evaluated against recovery's own view
|
|
781
|
+
// of the drain AND of credential ownership, rather than the one captured before
|
|
782
|
+
// decryption. Omitting ownership here would reopen the fence the first preview
|
|
783
|
+
// closes, on the one path that re-previews after the route may have moved.
|
|
784
|
+
deniedModelAccountIds: cachedDeniedCodexAccountIdsForModel(modelId, previewNow, {
|
|
785
|
+
excludeAccountIds: recoveryRequestScopedMainCredential
|
|
786
|
+
|| recoveryNativeMainBlocked
|
|
787
|
+
|| recoverySelectionAdmission?.mainProfileDraining === true
|
|
788
|
+
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
789
|
+
: undefined,
|
|
790
|
+
}),
|
|
791
|
+
},
|
|
669
792
|
modelId,
|
|
670
793
|
poolLineage,
|
|
671
794
|
);
|
|
@@ -860,7 +983,12 @@ export async function prepareResponsesRequest(
|
|
|
860
983
|
// refusing the turn that shrinks the context would deadlock the client against the very
|
|
861
984
|
// limit this gate reports — it would be told to compact and then denied the compaction.
|
|
862
985
|
if (parsed._compactionRequest !== true) {
|
|
863
|
-
|
|
986
|
+
// A combo child is the one caller that can afford a strict gate: skipping a target it
|
|
987
|
+
// cannot fit is safe before any upstream bytes are sent, and the ladder continues. A
|
|
988
|
+
// direct request has nowhere to go, so it keeps the loose pathological-input gate.
|
|
989
|
+
const inputAdmission = options.comboAttempt
|
|
990
|
+
? checkComboTargetInputAdmission(parsed, route.provider, route.providerName, parsed.modelId, nativeContextLimits(config))
|
|
991
|
+
: checkInputAdmission(parsed, route.provider, route.providerName, parsed.modelId, nativeContextLimits(config));
|
|
864
992
|
if (!inputAdmission.admitted) {
|
|
865
993
|
// #1524: this is a LOCAL preflight refusal, not an upstream verdict. A policy or combo
|
|
866
994
|
// fallback must be able to skip this candidate and try one whose context window fits,
|
|
@@ -876,9 +1004,13 @@ export async function prepareResponsesRequest(
|
|
|
876
1004
|
return formatErrorResponse(
|
|
877
1005
|
413,
|
|
878
1006
|
"input_admission_refused",
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1007
|
+
inputAdmission.requiredOutputHeadroom !== undefined
|
|
1008
|
+
? `Estimated input (~${inputAdmission.estimatedTokens} tokens) plus ${inputAdmission.requiredOutputHeadroom} `
|
|
1009
|
+
+ `tokens of requested output headroom cannot fit the context window of ${parsed.modelId} `
|
|
1010
|
+
+ `(${inputAdmission.ceiling} tokens).`
|
|
1011
|
+
: `Estimated input (~${inputAdmission.estimatedTokens} tokens) is far past the context window `
|
|
1012
|
+
+ `of ${parsed.modelId} (${inputAdmission.ceiling} tokens). Start a new session or choose a `
|
|
1013
|
+
+ `model with a larger context window.`,
|
|
882
1014
|
);
|
|
883
1015
|
}
|
|
884
1016
|
}
|
|
@@ -897,7 +1029,18 @@ export async function prepareResponsesRequest(
|
|
|
897
1029
|
let substituteMainCredential = false;
|
|
898
1030
|
let callerAuthHeaders: Headers;
|
|
899
1031
|
{
|
|
900
|
-
|
|
1032
|
+
// #4778: uploaded files are scoped to the account that issued them, so a conversation
|
|
1033
|
+
// carrying live references must retain its binding across a voluntary quota move. Answered
|
|
1034
|
+
// from the body alone, by the same predicate the refusal guard uses, so the two can never
|
|
1035
|
+
// disagree about which conversations are in scope.
|
|
1036
|
+
const finalAuth = await resolveResponsesCodexAuth(
|
|
1037
|
+
req,
|
|
1038
|
+
config,
|
|
1039
|
+
route,
|
|
1040
|
+
options,
|
|
1041
|
+
credentialDomainWasRewritten,
|
|
1042
|
+
conversationCarriesUploadedFiles(parsed._rawBody),
|
|
1043
|
+
);
|
|
901
1044
|
if (!finalAuth.ok) return finalAuth.response;
|
|
902
1045
|
admissionState.authCtx = finalAuth.authCtx;
|
|
903
1046
|
selectedForwardHeaders = withClaudeNativeSession(finalAuth.headers, route.provider, options.claudeNativeSessionId);
|
|
@@ -921,6 +1064,14 @@ export async function prepareResponsesRequest(
|
|
|
921
1064
|
{
|
|
922
1065
|
const binding = conversationStateBindingFromAuth(admissionState.authCtx, poolAffinityKey);
|
|
923
1066
|
if (binding) {
|
|
1067
|
+
// Before the scrub, because a file reference is refused rather than removed and the
|
|
1068
|
+
// refusal has to happen while there is still no dispatch to undo.
|
|
1069
|
+
const refusal = accountChangeFileReferenceRefusal({
|
|
1070
|
+
body: parsed._rawBody,
|
|
1071
|
+
bindingKey: binding.bindingKey,
|
|
1072
|
+
servingAccountId: binding.accountId,
|
|
1073
|
+
});
|
|
1074
|
+
if (refusal) return refusal;
|
|
924
1075
|
applyAccountChangeConversationStateScrub({
|
|
925
1076
|
body: parsed._rawBody,
|
|
926
1077
|
parsed,
|