@bitkyc08/opencodex 2.72.0 → 2.73.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/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
- package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
- package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
- package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/native-exec.ts +28 -10
- package/src/adapters/devin/live-models.ts +5 -0
- package/src/adapters/kiro/reasoning.ts +2 -0
- package/src/adapters/openai-chat/messages.ts +4 -1
- package/src/adapters/openai-responses/passthrough.ts +12 -1
- package/src/adapters/openai-responses/reasoning.ts +15 -0
- package/src/bridge/internal.ts +12 -0
- package/src/cli/account-anthropic-threshold.ts +41 -0
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +32 -21
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +8 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/build-entries.ts +6 -5
- package/src/codex/catalog/derive-entry.ts +3 -4
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/parsing.ts +4 -1
- package/src/codex/catalog/pinned-models.ts +3 -1
- package/src/codex/catalog/provider-models.ts +22 -2
- package/src/codex/data/roster-pinned-models.json +178 -1
- package/src/codex/model-entitlements.ts +1 -1
- package/src/codex/native-profile-store.ts +2 -1
- package/src/combos/resolve.ts +16 -1
- package/src/config/subagent-models.ts +30 -5
- package/src/generated/compatibility-version.json +95 -67
- package/src/generated/model-metadata.ts +4 -4
- package/src/lib/account-selection-events.ts +28 -0
- package/src/lib/errors.ts +7 -5
- package/src/lib/keyring-native.ts +132 -0
- package/src/lib/provider-outbound.ts +13 -0
- package/src/lib/retry-delay.ts +10 -0
- package/src/oauth/anthropic-account-threshold.ts +13 -0
- package/src/oauth/anthropic-routing.ts +223 -52
- package/src/oauth/index.ts +4 -2
- package/src/oauth/store.ts +45 -3
- package/src/oauth/types.ts +2 -0
- package/src/providers/api-key-resolve.ts +2 -4
- package/src/providers/codebuddy-models.ts +6 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/label.ts +20 -1
- package/src/providers/quota/account-cache.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
- package/src/providers/quota/vendor-probes-oauth.ts +84 -57
- package/src/providers/quota.ts +45 -46
- package/src/providers/registry/entries-core.ts +3 -1
- package/src/providers/registry/entries-extended.ts +14 -1
- package/src/providers/registry/model-seeds.ts +7 -1
- package/src/server/claude-messages.ts +84 -27
- package/src/server/management/anthropic-account-threshold.ts +27 -0
- package/src/server/management/oauth-account-routes.ts +17 -4
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/messages-native-oauth.ts +20 -21
- package/src/server/messages-native.ts +39 -14
- package/src/server/relay-eager.ts +5 -2
- package/src/server/relay.ts +268 -82
- package/src/server/responses/adapter-continuation.ts +6 -0
- package/src/server/responses/adapter-delivery.ts +25 -7
- package/src/server/responses/adapter-dispatch.ts +31 -0
- package/src/server/responses/buffered-sse-json.ts +345 -0
- package/src/server/responses/core-codex-account.ts +4 -1
- package/src/server/responses/core-combo.ts +28 -0
- package/src/server/responses/core-lifetime.ts +4 -0
- package/src/server/responses/passthrough-delivery.ts +305 -10
- package/src/server/responses/passthrough-dispatch.ts +60 -25
- package/src/server/responses/passthrough-error.ts +3 -1
- package/src/server/responses/request-transport.ts +22 -5
- package/src/server/responses/terminal-error-redaction.ts +53 -0
- package/src/service/windows-taskxml.ts +20 -6
- package/src/service-manager-probe.ts +115 -6
- package/src/types/wire.ts +7 -0
- package/src/usage/expected-prices.ts +11 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isNativeControlResponse } from "./native-response-control";
|
|
2
|
+
import { createOutboundCredentialMask, createTerminalErrorRedactionBlockRewrite } from "./terminal-error-redaction";
|
|
2
3
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
3
4
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
4
5
|
import type { ResponsesTransport } from "./request-transport";
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
createSseInspector,
|
|
11
12
|
markEagerRelaySseResponse,
|
|
12
13
|
markNativePassthroughSseResponse,
|
|
14
|
+
markPreinspectedJsonResponse,
|
|
13
15
|
consumeForInspection,
|
|
14
16
|
consumeForResponseLogMetadata,
|
|
15
17
|
relaySseWithFailedTail,
|
|
@@ -32,7 +34,7 @@ import { recordSubagentQuotaFailureForThreadSpawn } from "../../codex/subagent-m
|
|
|
32
34
|
import { recordCodexUpstreamOutcome } from "../../codex/routing";
|
|
33
35
|
import { codexProbeLeaseId, codexProbeQuotaScope, codexTransientProbeGrant, releaseCodexAuthContextProbeLease } from "../../codex/auth-context";
|
|
34
36
|
import { consumeComboFailure } from "./core-combo-failure";
|
|
35
|
-
import { readDisplaySafeErrorText } from "./core-errors";
|
|
37
|
+
import { clientCancelledResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
36
38
|
import { streamingContextOverflowResponse, jsonContextOverflowResponse } from "./context-overflow";
|
|
37
39
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
38
40
|
import { rewriteUpstreamPolicyRefusal } from "./policy-refusal";
|
|
@@ -131,13 +133,94 @@ import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
|
131
133
|
import { idleDeadline } from "../../lib/abort";
|
|
132
134
|
import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
133
135
|
import { formatErrorResponse } from "../../bridge";
|
|
134
|
-
import {
|
|
136
|
+
import {
|
|
137
|
+
httpStatusFromTerminalError,
|
|
138
|
+
inspectResponseLogJson,
|
|
139
|
+
inspectResponseLogSsePayloadParsed,
|
|
140
|
+
} from "../request-log";
|
|
135
141
|
import { restoreRoutedCustomCallsInJson } from "../../responses/custom-tool-compat";
|
|
136
142
|
import { restoreRoutedToolSearchCallsInJson } from "../../responses/tool-search-compat";
|
|
137
143
|
import { responsesJsonToSseStream } from "../responses-json-events";
|
|
144
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
145
|
+
import {
|
|
146
|
+
bufferedResponsesReadOptions,
|
|
147
|
+
collectBufferedResponsesSse,
|
|
148
|
+
type BufferedResponsesSseFailure,
|
|
149
|
+
type BufferedResponsesSseResult,
|
|
150
|
+
} from "./buffered-sse-json";
|
|
138
151
|
|
|
139
152
|
const PLAINTEXT_V2_SSE_PREFIX_LIMIT = 4096;
|
|
140
153
|
|
|
154
|
+
/** Replay retained SSE in small views so the rewrite decoder never receives one 32 MiB chunk. */
|
|
155
|
+
function replayBufferedSse(bytes: Uint8Array): ReadableStream<Uint8Array> {
|
|
156
|
+
let offset = 0;
|
|
157
|
+
return new ReadableStream({
|
|
158
|
+
pull(controller) {
|
|
159
|
+
if (offset >= bytes.byteLength) {
|
|
160
|
+
controller.close();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const end = Math.min(bytes.byteLength, offset + 64 * 1024);
|
|
164
|
+
controller.enqueue(bytes.subarray(offset, end));
|
|
165
|
+
offset = end;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const BUFFERED_CONTEXT_ERROR_CODES = new Set([
|
|
171
|
+
"context_length_exceeded", "context_window_exceeded", "input_too_long",
|
|
172
|
+
]);
|
|
173
|
+
const BUFFERED_QUOTA_ERROR_CODES = new Set([
|
|
174
|
+
"usage_limit_exceeded", "usage_limit_reached", "rate_limit_exceeded", "insufficient_quota",
|
|
175
|
+
]);
|
|
176
|
+
const BUFFERED_OVERLOAD_ERROR_CODES = new Set(["server_is_overloaded"]);
|
|
177
|
+
|
|
178
|
+
function bufferedErrorDefaults(status: number): { type: string; code: string } {
|
|
179
|
+
switch (status) {
|
|
180
|
+
case 400: return { type: "invalid_request_error", code: "invalid_request_error" };
|
|
181
|
+
case 401: return { type: "authentication_error", code: "invalid_api_key" };
|
|
182
|
+
case 403: return { type: "permission_error", code: "permission_denied" };
|
|
183
|
+
case 429: return { type: "rate_limit_error", code: "rate_limit_exceeded" };
|
|
184
|
+
case 499: return { type: "client_closed_request", code: "client_closed_request" };
|
|
185
|
+
case 503: return { type: "server_error", code: "server_is_overloaded" };
|
|
186
|
+
default: return { type: "upstream_error", code: "upstream_server_error" };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Preserve only explicit, recognized bare-error classes; diagnostic wording never sets status. */
|
|
191
|
+
function bufferedBareErrorPresentation(
|
|
192
|
+
failure: BufferedResponsesSseFailure,
|
|
193
|
+
message: string,
|
|
194
|
+
): { status: number; error: { type: string; code: string; message: string } } {
|
|
195
|
+
if (failure.upstreamRefusalCode !== undefined) {
|
|
196
|
+
return {
|
|
197
|
+
status: 400,
|
|
198
|
+
error: { type: "invalid_request_error", code: failure.upstreamRefusalCode, message },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const explicitCode = failure.upstreamErrorCode;
|
|
202
|
+
const status = explicitCode !== undefined
|
|
203
|
+
? BUFFERED_CONTEXT_ERROR_CODES.has(explicitCode) ? 400
|
|
204
|
+
: BUFFERED_QUOTA_ERROR_CODES.has(explicitCode) ? 429
|
|
205
|
+
: BUFFERED_OVERLOAD_ERROR_CODES.has(explicitCode) ? 503
|
|
206
|
+
: httpStatusFromTerminalError({ code: explicitCode })
|
|
207
|
+
: httpStatusFromTerminalError({ type: failure.upstreamErrorType });
|
|
208
|
+
const defaults = bufferedErrorDefaults(status);
|
|
209
|
+
if (status === 502) return { status, error: { ...defaults, message } };
|
|
210
|
+
const explicitType = failure.upstreamErrorType;
|
|
211
|
+
const typeMatchesStatus = explicitType !== undefined
|
|
212
|
+
&& (httpStatusFromTerminalError({ type: explicitType }) === status
|
|
213
|
+
|| (explicitType === "server_error" && status === 503));
|
|
214
|
+
return {
|
|
215
|
+
status,
|
|
216
|
+
error: {
|
|
217
|
+
type: typeMatchesStatus ? explicitType! : defaults.type,
|
|
218
|
+
code: explicitCode ?? defaults.code,
|
|
219
|
+
message,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
141
224
|
/** Prefix-probe budget: bounds one silent gap and the whole probe alike. */
|
|
142
225
|
interface PlaintextV2SseProbeOptions {
|
|
143
226
|
timeoutMs: number;
|
|
@@ -348,10 +431,12 @@ export async function deliverPassthroughResponse(
|
|
|
348
431
|
const { requestBindings } = transportState;
|
|
349
432
|
|
|
350
433
|
let upstreamResponse = nativeExchange.upstreamResponse;
|
|
434
|
+
const canonicalBufferedJson = clientRequestedStream !== true
|
|
435
|
+
&& isCanonicalOpenAiForwardProvider(route.provider);
|
|
351
436
|
const originalContentType = upstreamResponse.headers.get("content-type");
|
|
352
437
|
if (isUsageDebugEnabled() && originalContentType) logCtx.usageDebugContentType = originalContentType;
|
|
353
438
|
if (responseEffects.plaintextV2AgentMessageToolNames.size > 0
|
|
354
|
-
&& upstreamResponse.ok && upstreamResponse.body && parsed.stream
|
|
439
|
+
&& upstreamResponse.ok && upstreamResponse.body && (parsed.stream || canonicalBufferedJson)
|
|
355
440
|
&& !originalContentType?.toLowerCase().includes("text/event-stream")
|
|
356
441
|
&& !originalContentType?.toLowerCase().includes("application/json")
|
|
357
442
|
&& !isCodexWsUpstreamResponse(upstreamResponse)
|
|
@@ -372,7 +457,11 @@ export async function deliverPassthroughResponse(
|
|
|
372
457
|
// reach this fallback only after their first Responses event is confirmed.
|
|
373
458
|
const passthroughCt = headers.get("content-type")?.toLowerCase();
|
|
374
459
|
const isEventStream = passthroughCt?.includes("text/event-stream")
|
|
375
|
-
|| (responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
460
|
+
|| (responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
461
|
+
&& upstreamResponse.ok
|
|
462
|
+
&& !!upstreamResponse.body
|
|
463
|
+
&& !passthroughCt
|
|
464
|
+
&& (parsed.stream || canonicalBufferedJson));
|
|
376
465
|
const recordTerminalOutcome = codexForwardTerminalOutcomeRecorder(
|
|
377
466
|
config,
|
|
378
467
|
admissionState.authCtx,
|
|
@@ -522,10 +611,13 @@ export async function deliverPassthroughResponse(
|
|
|
522
611
|
const grokUpstreamEchoEnabled = isXaiResponsesDestination(route.provider)
|
|
523
612
|
&& responsesRequestMayReplayToolOutput(parsed._rawBody);
|
|
524
613
|
if (isEventStream && upstreamResponse.body) {
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
614
|
+
// Streaming clients commit the serving route once the upstream accepted the turn; retaining
|
|
615
|
+
// request state until their body ends would change the existing relay contract. The canonical
|
|
616
|
+
// non-streaming fold below is different: nothing is published yet, so it defers this mutation
|
|
617
|
+
// until both the raw and rewritten terminal snapshots validate.
|
|
618
|
+
if (!canonicalBufferedJson) {
|
|
619
|
+
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
620
|
+
}
|
|
529
621
|
const terminalRepairPolicy = route.staticPolicy.model.responsesTerminalRepair;
|
|
530
622
|
// #3761: opt-in hosted-web-search bridge. Codex always declares the hosted web_search tool,
|
|
531
623
|
// and this branch relays that declaration on the assumption the destination executes it.
|
|
@@ -665,7 +757,9 @@ export async function deliverPassthroughResponse(
|
|
|
665
757
|
// are not the Responses wire shapes the snapshot must mirror.
|
|
666
758
|
// Only validated client blocks may publish plaintext continuation state.
|
|
667
759
|
// Raw inspection precedes rewriting on eager relays, so it cannot own this write.
|
|
668
|
-
const plaintextInspector = !
|
|
760
|
+
const plaintextInspector = !canonicalBufferedJson
|
|
761
|
+
&& !grokUpstreamEchoEnabled
|
|
762
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size > 0
|
|
669
763
|
? createSseInspector({ onCompletedResponse: rememberPassthroughResponseChecked })
|
|
670
764
|
: undefined;
|
|
671
765
|
const plaintextEncoder = plaintextInspector ? new TextEncoder() : undefined;
|
|
@@ -675,6 +769,7 @@ export async function deliverPassthroughResponse(
|
|
|
675
769
|
return [block];
|
|
676
770
|
}, { dispose: () => plaintextInspector.dispose() })
|
|
677
771
|
: undefined;
|
|
772
|
+
const maskCredential = createOutboundCredentialMask(nativeExchange.request.headers);
|
|
678
773
|
const blockRewrites = [
|
|
679
774
|
payloadRewrites.length > 0
|
|
680
775
|
? payloadRewriteAsBlockRewrite(composeSsePayloadRewrites(...payloadRewrites))
|
|
@@ -742,12 +837,211 @@ export async function deliverPassthroughResponse(
|
|
|
742
837
|
rememberPassthroughResponse ? rememberPassthroughResponseChecked : undefined,
|
|
743
838
|
)
|
|
744
839
|
: undefined,
|
|
840
|
+
createTerminalErrorRedactionBlockRewrite(nativeExchange.request.headers, maskCredential),
|
|
745
841
|
rememberPlaintextBlock,
|
|
746
842
|
].filter((rewrite): rewrite is NonNullable<typeof rewrite> => rewrite !== undefined);
|
|
747
843
|
const clientBlockRewrite = blockRewrites.length > 0
|
|
748
844
|
? composeSseBlockRewrites(...blockRewrites)
|
|
749
845
|
: undefined;
|
|
750
846
|
const needsClientRewrite = clientBlockRewrite !== undefined;
|
|
847
|
+
|
|
848
|
+
if (canonicalBufferedJson) {
|
|
849
|
+
const signal = options.abortSignal ?? req.signal;
|
|
850
|
+
const bufferedRead = bufferedResponsesReadOptions(resolveStallTimeoutMs(
|
|
851
|
+
config.stallTimeoutSec,
|
|
852
|
+
{ localUpstream: nativeExchange.localUpstream },
|
|
853
|
+
));
|
|
854
|
+
const failBufferedTurn = (
|
|
855
|
+
message: string,
|
|
856
|
+
failure?: BufferedResponsesSseFailure,
|
|
857
|
+
): Response => {
|
|
858
|
+
upstream.abort(new Error(message));
|
|
859
|
+
const upstreamError = failure?.upstreamError;
|
|
860
|
+
const upstreamRefusalCode = failure?.upstreamRefusalCode;
|
|
861
|
+
const rawPresentation = upstreamError === undefined || failure === undefined
|
|
862
|
+
? undefined
|
|
863
|
+
: bufferedBareErrorPresentation(failure, upstreamError);
|
|
864
|
+
const presentation = rawPresentation === undefined ? undefined : {
|
|
865
|
+
status: rawPresentation.status,
|
|
866
|
+
error: {
|
|
867
|
+
type: maskCredential(rawPresentation.error.type),
|
|
868
|
+
code: maskCredential(rawPresentation.error.code),
|
|
869
|
+
message: maskCredential(rawPresentation.error.message),
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
if (upstreamError !== undefined) {
|
|
873
|
+
// The streaming relay turns a bare `error` event into a terminal failure. Mirror that
|
|
874
|
+
// verdict for JSON callers before recording/formatting it, or a provider refusal is
|
|
875
|
+
// misreported as a retryable generic 502 and clients can replay a rejected turn.
|
|
876
|
+
const error = presentation!.error;
|
|
877
|
+
const terminalPayload = {
|
|
878
|
+
type: "response.failed",
|
|
879
|
+
response: { status: "failed", error, last_error: error },
|
|
880
|
+
};
|
|
881
|
+
// Classification comes only from the bounded structured type/code above. A bare
|
|
882
|
+
// transport reset can contain misleading copy such as "invalid api key"; letting the
|
|
883
|
+
// log's message fallback reinterpret it would retire a healthy credential.
|
|
884
|
+
logCtx.terminalHttpStatus = presentation!.status;
|
|
885
|
+
noteInspectedPayload(terminalPayload);
|
|
886
|
+
inspectResponseLogSsePayloadParsed(
|
|
887
|
+
logCtx,
|
|
888
|
+
JSON.stringify(terminalPayload),
|
|
889
|
+
terminalPayload,
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
const failureStatus = logCtx.terminalHttpStatus ?? 502;
|
|
893
|
+
if (recordTerminalOutcomes) {
|
|
894
|
+
logCtx.transportPhase = "mid_stream";
|
|
895
|
+
logCtx.terminalSource = "synthetic";
|
|
896
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
897
|
+
terminalRecorder?.("failed", failureStatus);
|
|
898
|
+
options.onNativePassthroughTerminal?.("failed");
|
|
899
|
+
}
|
|
900
|
+
if (upstreamError !== undefined) {
|
|
901
|
+
const failureHeaders = new Headers(headers);
|
|
902
|
+
failureHeaders.set("content-type", "application/json");
|
|
903
|
+
failureHeaders.delete("content-length");
|
|
904
|
+
failureHeaders.delete("content-encoding");
|
|
905
|
+
if (upstreamRefusalCode !== undefined) failureHeaders.delete("retry-after");
|
|
906
|
+
const error = presentation!.error;
|
|
907
|
+
return formatPassthroughUpstreamError(
|
|
908
|
+
failureStatus,
|
|
909
|
+
JSON.stringify({ error, ...(upstreamRefusalCode === undefined ? {} : { retryable: false }) }),
|
|
910
|
+
{
|
|
911
|
+
headers: failureHeaders,
|
|
912
|
+
suppressRetryAfter: upstreamRefusalCode !== undefined,
|
|
913
|
+
},
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
return formatErrorResponse(502, "upstream_error", message);
|
|
917
|
+
};
|
|
918
|
+
let raw: BufferedResponsesSseResult | undefined = await collectBufferedResponsesSse(
|
|
919
|
+
passthroughSseBody,
|
|
920
|
+
upstream,
|
|
921
|
+
{ signal, terminalBoundary: codexSafetyBufferingOptions, read: bufferedRead, retainTranscript: true },
|
|
922
|
+
);
|
|
923
|
+
if (!raw.ok) {
|
|
924
|
+
if (raw.kind === "aborted" || signal.aborted) {
|
|
925
|
+
responseEffects.responseCompletionCancelled = true;
|
|
926
|
+
options.onNativePassthroughCancel?.();
|
|
927
|
+
return clientCancelledResponse();
|
|
928
|
+
}
|
|
929
|
+
const message = raw.kind === "oversized"
|
|
930
|
+
? "upstream SSE response exceeded the safe body limit"
|
|
931
|
+
: raw.kind === "timeout"
|
|
932
|
+
? "upstream SSE response stalled before completing"
|
|
933
|
+
: raw.kind === "malformed" || raw.kind === "missing_terminal"
|
|
934
|
+
? "upstream SSE response ended without one valid terminal response"
|
|
935
|
+
: "upstream SSE response failed before a valid terminal response";
|
|
936
|
+
return failBufferedTurn(message, raw);
|
|
937
|
+
}
|
|
938
|
+
if (!raw.bytes) return failBufferedTurn("upstream SSE transcript was not retained for rewriting");
|
|
939
|
+
let rawBytes: Uint8Array | undefined = raw.bytes;
|
|
940
|
+
raw = undefined; // Release the raw reconstruction graph before building the client copy.
|
|
941
|
+
|
|
942
|
+
// Reuse the ordinary client rewrite chain against the bounded transcript. The raw pass
|
|
943
|
+
// above validates before any continuation or outcome side effect; this second validation
|
|
944
|
+
// means a rewrite failure cannot publish a turn the non-streaming caller never received.
|
|
945
|
+
let bufferedClientBody: ReadableStream<Uint8Array> | undefined = clientBlockRewrite
|
|
946
|
+
? relaySseWithBlockRewrite(
|
|
947
|
+
replayBufferedSse(rawBytes),
|
|
948
|
+
clientBlockRewrite,
|
|
949
|
+
translatorBudget,
|
|
950
|
+
)
|
|
951
|
+
: replayBufferedSse(rawBytes);
|
|
952
|
+
const client = await collectBufferedResponsesSse(bufferedClientBody, upstream, {
|
|
953
|
+
signal,
|
|
954
|
+
read: bufferedRead,
|
|
955
|
+
retainTranscript: false,
|
|
956
|
+
});
|
|
957
|
+
bufferedClientBody = undefined;
|
|
958
|
+
if (!client.ok) {
|
|
959
|
+
if (client.kind === "aborted" || signal.aborted) {
|
|
960
|
+
responseEffects.responseCompletionCancelled = true;
|
|
961
|
+
options.onNativePassthroughCancel?.();
|
|
962
|
+
return clientCancelledResponse();
|
|
963
|
+
}
|
|
964
|
+
const message = client.kind === "oversized"
|
|
965
|
+
? "rewritten SSE response exceeded the safe body limit"
|
|
966
|
+
: client.kind === "timeout"
|
|
967
|
+
? "rewritten SSE response stalled before completing"
|
|
968
|
+
: "rewritten SSE response did not contain one valid terminal response";
|
|
969
|
+
return failBufferedTurn(message, client);
|
|
970
|
+
}
|
|
971
|
+
if (Date.now() >= bufferedRead.deadlineAt) {
|
|
972
|
+
return failBufferedTurn("buffered Responses turn exceeded the safe total deadline");
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const cancelAfterValidation = (): Response => {
|
|
976
|
+
responseEffects.responseCompletionCancelled = true;
|
|
977
|
+
options.onNativePassthroughCancel?.();
|
|
978
|
+
return clientCancelledResponse();
|
|
979
|
+
};
|
|
980
|
+
if (signal.aborted) return cancelAfterValidation();
|
|
981
|
+
|
|
982
|
+
// Effects intentionally run only after both bounded validations. They inspect the same
|
|
983
|
+
// upstream-facing transcript as the streaming relay and fire once; continuation storage
|
|
984
|
+
// receives the final client-visible response once after all restorations succeeded.
|
|
985
|
+
const reportNativeTerminal = recordTerminalOutcomes
|
|
986
|
+
? (status: ResponsesTerminalStatus, httpStatusOverride?: number) => {
|
|
987
|
+
if (signal.aborted) return;
|
|
988
|
+
terminalRecorder?.(status, httpStatusOverride);
|
|
989
|
+
if (status === "failed" || status === "incomplete") {
|
|
990
|
+
const quotaFailureMessage = [httpStatusOverride, logCtx.terminalHttpStatus]
|
|
991
|
+
.find(value => value === 429 || value === 402);
|
|
992
|
+
if (!isFixedCodexAccount(admissionState.authCtx) && quotaFailureMessage !== undefined) {
|
|
993
|
+
recordSubagentQuotaFailureForThreadSpawn(
|
|
994
|
+
req.headers,
|
|
995
|
+
subagentQuotaFailureModel,
|
|
996
|
+
quotaFailureMessage,
|
|
997
|
+
config,
|
|
998
|
+
requestState.subagentFallbackAccountId,
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
options.onNativePassthroughTerminal?.(status);
|
|
1003
|
+
}
|
|
1004
|
+
: undefined;
|
|
1005
|
+
const effectInspector = createSseInspector({
|
|
1006
|
+
onTerminal: reportNativeTerminal,
|
|
1007
|
+
logCtx,
|
|
1008
|
+
onParsedPayload: noteInspectedPayload,
|
|
1009
|
+
onFirstOutput: options.onFirstOutput,
|
|
1010
|
+
pinCompletedResponseIdToFirstSeen: githubCopilotRepairEnabled,
|
|
1011
|
+
});
|
|
1012
|
+
try {
|
|
1013
|
+
// The deferred effects pass must not turn a bounded 32 MiB transcript into one long
|
|
1014
|
+
// event-loop monopoly. Keep decoder input small and yield between MiB groups; the
|
|
1015
|
+
// aggregate frame cap was already enforced by both validation passes.
|
|
1016
|
+
for (let offset = 0; offset < rawBytes.byteLength; offset += 64 * 1024) {
|
|
1017
|
+
if (signal.aborted) return cancelAfterValidation();
|
|
1018
|
+
effectInspector.feed(rawBytes.subarray(offset, Math.min(rawBytes.byteLength, offset + 64 * 1024)));
|
|
1019
|
+
if (offset > 0 && offset % (1024 * 1024) === 0) {
|
|
1020
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
1021
|
+
if (signal.aborted) return cancelAfterValidation();
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
if (signal.aborted) return cancelAfterValidation();
|
|
1025
|
+
effectInspector.finish();
|
|
1026
|
+
} finally {
|
|
1027
|
+
effectInspector.dispose();
|
|
1028
|
+
}
|
|
1029
|
+
if (signal.aborted) return cancelAfterValidation();
|
|
1030
|
+
commitReasoningReplayServingRoute(nativeExchange.request.headers);
|
|
1031
|
+
rawBytes = undefined;
|
|
1032
|
+
if (client.terminal.status === "completed") {
|
|
1033
|
+
rememberPassthroughResponseChecked(client.terminal.response);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const jsonHeaders = sanitizePassthroughHeaders(headers, codexSafetyBufferingOptions);
|
|
1037
|
+
jsonHeaders.set("content-type", "application/json");
|
|
1038
|
+
return markPreinspectedJsonResponse(new Response(JSON.stringify(client.terminal.response), {
|
|
1039
|
+
status: upstreamResponse.status,
|
|
1040
|
+
statusText: upstreamResponse.statusText,
|
|
1041
|
+
headers: jsonHeaders,
|
|
1042
|
+
}));
|
|
1043
|
+
}
|
|
1044
|
+
|
|
751
1045
|
const relayPlatform = relayPlatformForTests ?? process.platform;
|
|
752
1046
|
// #864: win32 rewrite traffic must never enter the tee()+JS-pull chain
|
|
753
1047
|
// (Bun#32111 JS-sink segfault — text frames pass, the terminal block is
|
|
@@ -829,6 +1123,7 @@ export async function deliverPassthroughResponse(
|
|
|
829
1123
|
}, {
|
|
830
1124
|
clientGoneSignal: options.abortSignal,
|
|
831
1125
|
terminalBoundary: codexSafetyBufferingOptions,
|
|
1126
|
+
maskCredential,
|
|
832
1127
|
...(inlineEagerRewrite ? { rewriteBudget: translatorBudget } : {}),
|
|
833
1128
|
...(logCtx.upstreamError === undefined ? {} : { upstreamError: logCtx.upstreamError }),
|
|
834
1129
|
});
|
|
@@ -922,7 +1217,7 @@ export async function deliverPassthroughResponse(
|
|
|
922
1217
|
responseEffects.responseCompletionCancelled = true;
|
|
923
1218
|
clientGone.abort(reason);
|
|
924
1219
|
},
|
|
925
|
-
{ upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions },
|
|
1220
|
+
{ upstreamError: logCtx.upstreamError, terminalBoundary: codexSafetyBufferingOptions, maskCredential },
|
|
926
1221
|
);
|
|
927
1222
|
return markNativePassthroughSseResponse(new Response(clientBody, {
|
|
928
1223
|
status: upstreamResponse.status,
|
|
@@ -137,7 +137,8 @@ import { resolveWireProtocolOverride } from "../adapter-resolve";
|
|
|
137
137
|
import { refreshPoolForwardAuth, refreshNativeMainForwardAuth, withClaudeNativeSession } from "./core-auth";
|
|
138
138
|
import { bindRouteReasoningReplayScope } from "./core-replay";
|
|
139
139
|
import type { OAuthAccessSnapshot } from "../../oauth";
|
|
140
|
-
import { publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
140
|
+
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../../oauth";
|
|
141
|
+
import { AnthropicAccountCooldownError } from "../../oauth/anthropic-routing";
|
|
141
142
|
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
142
143
|
import {
|
|
143
144
|
hasEligibleGenericOAuthFailoverTarget,
|
|
@@ -260,6 +261,18 @@ export async function preparePassthroughExchange(
|
|
|
260
261
|
const codexSafetyBufferingOptions = isCanonicalOpenAiForwardProvider(route.provider)
|
|
261
262
|
? codexSafetyBufferingFilterOptions(config)
|
|
262
263
|
: undefined;
|
|
264
|
+
// The canonical ChatGPT adapter coerces only its final outbound copy to `stream:true` so the
|
|
265
|
+
// separately captured client preference can still select JSON delivery. Recovery runs before
|
|
266
|
+
// delivery, though, and must classify the bytes the upstream was actually asked to send:
|
|
267
|
+
// ChatGPT may omit Content-Type, so consulting `parsed.stream` here would mistake its SSE body
|
|
268
|
+
// for non-streaming JSON and skip both opaque-state and safe-reset recovery.
|
|
269
|
+
const upstreamRequestsStream = parsed.stream === true
|
|
270
|
+
|| isCanonicalOpenAiForwardProvider(route.provider);
|
|
271
|
+
// A JSON client still needs the canonical destination's HTTP/SSE response so delivery can
|
|
272
|
+
// validate and fold it. Letting the forced `stream:true` body select the WebSocket transport
|
|
273
|
+
// bypasses manual HTTP redirects and can turn a real 3xx into a connect timeout.
|
|
274
|
+
const canonicalBufferedJson = clientRequestedStream !== true
|
|
275
|
+
&& isCanonicalOpenAiForwardProvider(route.provider);
|
|
263
276
|
const imageGenCallAliases = route.provider.authMode === "forward"
|
|
264
277
|
? new Map<string, { namespace: string; name: string }>()
|
|
265
278
|
: imageGenToolCallAliases(toolBridgeMaps.toolNsMap, parsed._rawBody, translatorBudget);
|
|
@@ -881,7 +894,23 @@ export async function preparePassthroughExchange(
|
|
|
881
894
|
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
882
895
|
return formatErrorResponse(429, "request_send_budget_exhausted", err.message);
|
|
883
896
|
}
|
|
884
|
-
const
|
|
897
|
+
const refusal = unwrapUpstreamRetryEvidenceError(err);
|
|
898
|
+
// Pacing may outlive the selected account's admission. No fetch occurred, so do
|
|
899
|
+
// not turn an operator pause into a 502 or charge it to host/account health.
|
|
900
|
+
if (refusal instanceof OAuthAccountPausedError || refusal instanceof OAuthLoginRequiredError || refusal instanceof AnthropicAccountCooldownError) {
|
|
901
|
+
releaseUpstreamHostAdmission(nativeHostState.lease);
|
|
902
|
+
nativeHostState.lease = null;
|
|
903
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
904
|
+
if (refusal instanceof OAuthLoginRequiredError) {
|
|
905
|
+
return formatErrorResponse(401, "authentication_error", publicOAuthAuthenticationErrorMessage(refusal));
|
|
906
|
+
}
|
|
907
|
+
if (refusal instanceof AnthropicAccountCooldownError) {
|
|
908
|
+
return formatErrorResponse(429, "rate_limit_error", refusal.message,
|
|
909
|
+
refusal.retryAfterSeconds === null ? undefined : { retryAfter: String(refusal.retryAfterSeconds) });
|
|
910
|
+
}
|
|
911
|
+
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
|
|
912
|
+
}
|
|
913
|
+
const localRefusal = mapCodexAuthContextErrorToResponse(refusal, {
|
|
885
914
|
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
886
915
|
});
|
|
887
916
|
if (localRefusal) {
|
|
@@ -944,8 +973,9 @@ export async function preparePassthroughExchange(
|
|
|
944
973
|
method: request.method,
|
|
945
974
|
headers: request.headers,
|
|
946
975
|
body: request.body,
|
|
947
|
-
}, recovery), upstream.signal, connectMs,
|
|
976
|
+
}, recovery), upstream.signal, connectMs, upstreamRequestsStream,
|
|
948
977
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
978
|
+
httpOnly: canonicalBufferedJson,
|
|
949
979
|
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
950
980
|
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
951
981
|
? options.nativeControl : undefined,
|
|
@@ -1048,12 +1078,13 @@ export async function preparePassthroughExchange(
|
|
|
1048
1078
|
method: request.method,
|
|
1049
1079
|
headers: request.headers,
|
|
1050
1080
|
body: request.body,
|
|
1051
|
-
}, innerRecovery), upstream.signal, connectMs,
|
|
1081
|
+
}, innerRecovery), upstream.signal, connectMs, upstreamRequestsStream,
|
|
1052
1082
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1083
|
+
httpOnly: canonicalBufferedJson,
|
|
1084
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1085
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1086
|
+
? options.nativeControl : undefined,
|
|
1087
|
+
dispatchOverride: oauthDispatch(request),
|
|
1057
1088
|
providerName: route.providerName,
|
|
1058
1089
|
modelId: route.modelId,
|
|
1059
1090
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
@@ -1156,6 +1187,7 @@ export async function preparePassthroughExchange(
|
|
|
1156
1187
|
// rather than a second one to announce.
|
|
1157
1188
|
const oauthReplayExecutor = storedPoolReplayDispatchNotifier(
|
|
1158
1189
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1190
|
+
httpOnly: canonicalBufferedJson,
|
|
1159
1191
|
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1160
1192
|
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1161
1193
|
? options.nativeControl : undefined,
|
|
@@ -1185,7 +1217,7 @@ export async function preparePassthroughExchange(
|
|
|
1185
1217
|
}, recovery),
|
|
1186
1218
|
upstream.signal,
|
|
1187
1219
|
connectMs,
|
|
1188
|
-
|
|
1220
|
+
upstreamRequestsStream,
|
|
1189
1221
|
oauthReplayExecutor,
|
|
1190
1222
|
route.provider.authMode === "forward",
|
|
1191
1223
|
).then(adoptObservedResponse);
|
|
@@ -1303,12 +1335,13 @@ export async function preparePassthroughExchange(
|
|
|
1303
1335
|
method: request.method,
|
|
1304
1336
|
headers: request.headers,
|
|
1305
1337
|
body: request.body,
|
|
1306
|
-
}, recovery), upstream.signal, connectMs,
|
|
1338
|
+
}, recovery), upstream.signal, connectMs, upstreamRequestsStream,
|
|
1307
1339
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1340
|
+
httpOnly: canonicalBufferedJson,
|
|
1341
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1342
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1343
|
+
? options.nativeControl : undefined,
|
|
1344
|
+
dispatchOverride: oauthDispatch(request),
|
|
1312
1345
|
providerName: route.providerName,
|
|
1313
1346
|
modelId: route.modelId,
|
|
1314
1347
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
@@ -1437,12 +1470,13 @@ export async function preparePassthroughExchange(
|
|
|
1437
1470
|
method: request.method,
|
|
1438
1471
|
headers: request.headers,
|
|
1439
1472
|
body: request.body,
|
|
1440
|
-
}, recovery), upstream.signal, connectMs,
|
|
1473
|
+
}, recovery), upstream.signal, connectMs, upstreamRequestsStream,
|
|
1441
1474
|
providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1475
|
+
httpOnly: canonicalBufferedJson,
|
|
1476
|
+
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1477
|
+
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
1478
|
+
? options.nativeControl : undefined,
|
|
1479
|
+
dispatchOverride: oauthDispatch(request),
|
|
1446
1480
|
providerName: route.providerName,
|
|
1447
1481
|
modelId: route.modelId,
|
|
1448
1482
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
@@ -1554,7 +1588,8 @@ export async function preparePassthroughExchange(
|
|
|
1554
1588
|
upstream,
|
|
1555
1589
|
connectMs,
|
|
1556
1590
|
passthroughEstimate,
|
|
1557
|
-
stream:
|
|
1591
|
+
stream: upstreamRequestsStream,
|
|
1592
|
+
httpOnly: canonicalBufferedJson,
|
|
1558
1593
|
onResponse: (response, retryAuthCtx, retryRequest) => {
|
|
1559
1594
|
adoptCodexWsStage(response);
|
|
1560
1595
|
captureAffinityResponse(
|
|
@@ -1604,7 +1639,7 @@ export async function preparePassthroughExchange(
|
|
|
1604
1639
|
const recoveryContentType = upstreamResponse.headers.get("content-type")?.toLowerCase() ?? "";
|
|
1605
1640
|
const streamedFunctionOutputCandidate = upstreamResponse.ok
|
|
1606
1641
|
&& !!upstreamResponse.body
|
|
1607
|
-
&& (recoveryContentType.includes("text/event-stream") || (!recoveryContentType &&
|
|
1642
|
+
&& (recoveryContentType.includes("text/event-stream") || (!recoveryContentType && upstreamRequestsStream))
|
|
1608
1643
|
&& !opaqueBlobRecoveryGuard.attempted
|
|
1609
1644
|
&& !configuredTransientSendBudgetExhausted()
|
|
1610
1645
|
&& outboundResponsesBodyCarriesEncryptedFunctionOutput(request.body);
|
|
@@ -1622,7 +1657,7 @@ export async function preparePassthroughExchange(
|
|
|
1622
1657
|
if (decryptRejection) preflightLog.upstreamError = ENCRYPTED_FUNCTION_OUTPUT_REJECTION;
|
|
1623
1658
|
return decryptRejection;
|
|
1624
1659
|
}, {
|
|
1625
|
-
allowMissingContentType: !recoveryContentType &&
|
|
1660
|
+
allowMissingContentType: !recoveryContentType && upstreamRequestsStream,
|
|
1626
1661
|
replayReadErrors: true,
|
|
1627
1662
|
});
|
|
1628
1663
|
if (options.abortSignal?.aborted) return transportFailureResponse(options.abortSignal.reason);
|
|
@@ -1772,7 +1807,7 @@ export async function preparePassthroughExchange(
|
|
|
1772
1807
|
&& !(options.nativeControl && options.inboundTransport === "websocket")
|
|
1773
1808
|
&& ambiguousResend() !== undefined
|
|
1774
1809
|
&& remainingTransientSendBudget(transientSendAttempts()) > 0
|
|
1775
|
-
&& (streamRecoveryContentType.includes("text/event-stream") || (!streamRecoveryContentType &&
|
|
1810
|
+
&& (streamRecoveryContentType.includes("text/event-stream") || (!streamRecoveryContentType && upstreamRequestsStream));
|
|
1776
1811
|
if (protocolRecoveryCandidate) {
|
|
1777
1812
|
upstreamResponse = deferProtocolSafeResetRecovery(
|
|
1778
1813
|
upstreamResponse,
|
|
@@ -1791,11 +1826,11 @@ export async function preparePassthroughExchange(
|
|
|
1791
1826
|
authorize: () => authorizeResendForRecovery(stage, "connection-reset", ambiguousResend()).allowed,
|
|
1792
1827
|
acceptResponse: candidate => {
|
|
1793
1828
|
const type = candidate.headers.get("content-type")?.toLowerCase() ?? "";
|
|
1794
|
-
return type.includes("text/event-stream") || (!type &&
|
|
1829
|
+
return type.includes("text/event-stream") || (!type && upstreamRequestsStream);
|
|
1795
1830
|
},
|
|
1796
1831
|
},
|
|
1797
1832
|
),
|
|
1798
|
-
{ allowMissingContentType: !streamRecoveryContentType &&
|
|
1833
|
+
{ allowMissingContentType: !streamRecoveryContentType && upstreamRequestsStream },
|
|
1799
1834
|
);
|
|
1800
1835
|
}
|
|
1801
1836
|
break;
|
|
@@ -79,6 +79,8 @@ export function formatPassthroughUpstreamError(
|
|
|
79
79
|
statusText?: string;
|
|
80
80
|
headers?: Headers;
|
|
81
81
|
now?: number;
|
|
82
|
+
/** Caller holds an authoritative fatal verdict; neither inherited nor inferred waits apply. */
|
|
83
|
+
suppressRetryAfter?: boolean;
|
|
82
84
|
/**
|
|
83
85
|
* Provenance from the caller that still holds the response: this body is a refusal this
|
|
84
86
|
* proxy synthesized. The body check below is the fallback for a re-wrapped body, and it
|
|
@@ -96,7 +98,7 @@ export function formatPassthroughUpstreamError(
|
|
|
96
98
|
const replayRefusal = options?.replayRefusal === true || isReplayRefusalBody(trimmed);
|
|
97
99
|
// Two different reasons to answer with no wait at all, handled the same way: a hard policy
|
|
98
100
|
// block will not become servable, and a refusal we made was never a rate limit.
|
|
99
|
-
const suppressRetryAfter = cyberPolicyFailure || replayRefusal;
|
|
101
|
+
const suppressRetryAfter = options?.suppressRetryAfter === true || cyberPolicyFailure || replayRefusal;
|
|
100
102
|
const resolved = suppressRetryAfter
|
|
101
103
|
? undefined
|
|
102
104
|
: resolveClientRetryAfter({
|