@bitkyc08/opencodex 2.87.0 → 2.88.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-ZfYbLHkB.js → App-CHC89EXQ.js} +3 -3
- package/gui/dist/assets/{Tray-DCd-A1az.js → Tray-B5bC7AWL.js} +1 -1
- package/gui/dist/assets/index-B5bsvxor.js +94 -0
- package/gui/dist/assets/{quota-summary-ChvmKGOk.js → quota-summary-D8y2wTsB.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-rejection-memo.ts +71 -0
- package/src/adapters/anthropic-replay.ts +135 -0
- package/src/adapters/anthropic-tool-catalog.ts +11 -0
- package/src/adapters/anthropic.ts +76 -45
- package/src/adapters/google-progress-history.ts +53 -0
- package/src/adapters/google.ts +17 -1
- package/src/bridge/internal.ts +6 -2
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +7 -6
- package/src/claude/inbound-content-options.ts +66 -0
- package/src/claude/outbound.ts +13 -5
- package/src/cli/cross-home-owner.ts +16 -2
- package/src/cli/loopback-family.ts +31 -0
- package/src/codex/catalog/routed-gather.ts +3 -1
- package/src/codex/websocket-activity.ts +161 -0
- package/src/codex/websocket-registry.ts +25 -1
- package/src/config/opencode-zen-upgrade.ts +9 -8
- package/src/generated/compatibility-version.json +88 -52
- package/src/images/loop.ts +72 -21
- package/src/lib/request-failure-model.ts +2 -0
- package/src/oauth/generic-account-failover.ts +7 -4
- package/src/protocols/encoders/adapter-events.ts +5 -5
- package/src/protocols/encoders/messages.ts +2 -2
- package/src/providers/opencode-zen-rate-limit.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +50 -4
- package/src/routing/analytics.ts +7 -15
- package/src/server/claude-messages.ts +2 -1
- package/src/server/index/live-sideband.ts +3 -2
- package/src/server/index/serve-options.ts +11 -5
- package/src/server/index/websocket-handler.ts +18 -4
- package/src/server/index.ts +7 -7
- package/src/server/lifecycle.ts +13 -1
- package/src/server/messages-native.ts +16 -3
- package/src/server/responses/adapter-continuation.ts +34 -18
- package/src/server/responses/adapter-delivery.ts +53 -6
- package/src/server/responses/adapter-dispatch.ts +27 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
- package/src/server/responses/combo-stream-preflight.ts +3 -2
- package/src/server/responses/compaction-recovery.ts +2 -0
- package/src/server/responses/core-codex-account.ts +2 -2
- package/src/server/responses/core-combo.ts +3 -2
- package/src/server/responses/core-lifetime.ts +3 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +5 -1
- package/src/server/responses/passthrough-execution.ts +3 -2
- package/src/server/responses/request-prepare.ts +1 -0
- package/src/server/responses/response-effects.ts +1 -0
- package/src/server/responses/response-tool-ownership.ts +21 -0
- package/src/server/responses/sidecar-execution.ts +43 -7
- package/src/server/responses/sidecar-send-budget.ts +2 -2
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/ws-bridge.ts +2 -0
- package/src/types/opencode-zen.ts +9 -3
- package/src/types/request.ts +10 -2
- package/src/types/wire.ts +4 -2
- package/src/types.ts +1 -0
- package/src/usage/summary.ts +9 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/web-search/anthropic-history.ts +59 -0
- package/src/web-search/loop.ts +181 -68
- package/src/web-search/progress-stream.ts +11 -1
- package/gui/dist/assets/index-CfYDMj34.js +0 -94
package/src/server/index.ts
CHANGED
|
@@ -267,17 +267,17 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
267
267
|
startupOwnershipHomes = homes;
|
|
268
268
|
startupOwnershipStatePaths = statePaths;
|
|
269
269
|
} catch { /* inspection below stays unknown */ }
|
|
270
|
-
const startupCacheOwnership = inspectStartupOwnership(
|
|
270
|
+
const startupCacheOwnership = shouldSyncCodexOnStart(config) ? inspectStartupOwnership(
|
|
271
271
|
deps,
|
|
272
272
|
startupOwnershipHomes,
|
|
273
273
|
startupOwnershipStatePaths,
|
|
274
274
|
startupWindowsTaskListingCache,
|
|
275
|
-
);
|
|
275
|
+
) : null;
|
|
276
276
|
// Startup cache invalidation is best-effort and applies only when startup sync is enabled.
|
|
277
277
|
// Check OFF before taking K: on Windows K resolves SID + LocalAppData through PowerShell,
|
|
278
278
|
// and run 35093667426 exceeded healthy controls by 33.8 s against that 30 s child budget.
|
|
279
279
|
// The permit callback still re-reads intent under K to close a concurrent disable race.
|
|
280
|
-
if (
|
|
280
|
+
if (startupCacheOwnership?.ownership === "owned" && startupOwnershipHomes !== null) {
|
|
281
281
|
try {
|
|
282
282
|
const startupCodexHome = startupOwnershipHomes.codexHome;
|
|
283
283
|
// #1046: record whether this actually rewrote the cache. `handleStart` ORs this
|
|
@@ -549,12 +549,12 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
549
549
|
// clients; no Codex request can use this lifecycle in that state.
|
|
550
550
|
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
551
551
|
// between the two sites must not widen the service-install race.
|
|
552
|
-
const nativeOwnership = inspectStartupOwnership(
|
|
552
|
+
const nativeOwnership = shouldSyncCodexOnStart(config) ? inspectStartupOwnership(
|
|
553
553
|
deps,
|
|
554
554
|
startupOwnershipHomes,
|
|
555
555
|
startupOwnershipStatePaths,
|
|
556
|
-
);
|
|
557
|
-
const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
|
|
556
|
+
) : null;
|
|
557
|
+
const preparedNativeMainLifecycle = nativeOwnership !== null && nativeOwnership.ownership !== "foreign"
|
|
558
558
|
&& startupOwnershipHomes !== null
|
|
559
559
|
? prepareNativeMainStartupLifecycle(
|
|
560
560
|
deps.nativeMainStartup,
|
|
@@ -600,7 +600,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
600
600
|
return retryPreparedNativeMainLifecycle.start();
|
|
601
601
|
},
|
|
602
602
|
};
|
|
603
|
-
const nativeMainLifecycle: NativeMainStartupLifecycle =
|
|
603
|
+
const nativeMainLifecycle: NativeMainStartupLifecycle = nativeOwnership !== null
|
|
604
604
|
? nativeOwnership.ownership === "owned"
|
|
605
605
|
? preparedNativeMainLifecycle
|
|
606
606
|
? preparedNativeMainLifecycle.start()
|
package/src/server/lifecycle.ts
CHANGED
|
@@ -38,6 +38,7 @@ export interface ActiveTurnLease extends AdmissionLease {
|
|
|
38
38
|
attach(lease: AdmissionLease): void;
|
|
39
39
|
bindAbortSignal(signal: AbortSignal): () => void;
|
|
40
40
|
bindAbortController(ac: AbortController): void;
|
|
41
|
+
unbindAbortController(ac: AbortController): void;
|
|
41
42
|
beginCodexAccountSelection(): CodexAccountSelectionAdmission;
|
|
42
43
|
isTransferred(): boolean;
|
|
43
44
|
}
|
|
@@ -231,6 +232,17 @@ export function tryAdmitTurn(sessionLaneId?: string, clientSignal?: AbortSignal)
|
|
|
231
232
|
activeTurns.set(ac, lease);
|
|
232
233
|
lease.bindAbortSignal(ac.signal);
|
|
233
234
|
},
|
|
235
|
+
unbindAbortController(ac) {
|
|
236
|
+
if (!controllers.delete(ac)) return;
|
|
237
|
+
if (activeTurns.get(ac) === lease) activeTurns.delete(ac);
|
|
238
|
+
const listener = abortListeners.get(ac.signal);
|
|
239
|
+
if (listener) {
|
|
240
|
+
ac.signal.removeEventListener("abort", listener);
|
|
241
|
+
abortListeners.delete(ac.signal);
|
|
242
|
+
}
|
|
243
|
+
// A completed child relay cannot retire its still-running parent turn.
|
|
244
|
+
if (controllers.size === 0) lease.release();
|
|
245
|
+
},
|
|
234
246
|
beginCodexAccountSelection() {
|
|
235
247
|
const mainProfileDraining = nativeMainDrainOwners.size > 0;
|
|
236
248
|
let selectionActive = active && !mainProfileDraining;
|
|
@@ -327,7 +339,7 @@ export function unregisterTurn(ac: AbortController): void {
|
|
|
327
339
|
turnReleaseMisses += 1;
|
|
328
340
|
return;
|
|
329
341
|
}
|
|
330
|
-
lease.
|
|
342
|
+
lease.unbindAbortController(ac);
|
|
331
343
|
}
|
|
332
344
|
export function isDraining(): boolean { return shutdownDraining || temporaryDrainOwners.size > 0; }
|
|
333
345
|
export function getActiveTurnCount(): number { return turnGate.metrics().active; }
|
|
@@ -44,6 +44,7 @@ import { classifyError } from "../lib/errors";
|
|
|
44
44
|
import { relaySseWithPayloadRewrite } from "./sse-payload-rewrite";
|
|
45
45
|
import { redactSecretString } from "../lib/redact";
|
|
46
46
|
import { resolveClientRetryAfter } from "../lib/retry-after";
|
|
47
|
+
import { enrichOpenCodeZenUpstreamMessage } from "../providers/opencode-zen-rate-limit";
|
|
47
48
|
import { isTranslatorBudgetExceededError, type TranslatorBudget } from "../lib/translator-budget";
|
|
48
49
|
import {
|
|
49
50
|
applyReplayRefusalClientHeaders,
|
|
@@ -845,7 +846,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
845
846
|
upstream.abort();
|
|
846
847
|
return fail(499, "Client cancelled request", "api_error");
|
|
847
848
|
}
|
|
848
|
-
return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog), response.headers);
|
|
849
|
+
return retainUpstreamMessagesRequestId(nativeMessagesErrorResponse(response, bodyText, finishLog, route.providerName, activeProvider), response.headers);
|
|
849
850
|
}
|
|
850
851
|
|
|
851
852
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
@@ -999,7 +1000,7 @@ function isNativeOverflowError(error: Rec, message: string): boolean {
|
|
|
999
1000
|
* Messages path, so a client sees the same contract on either lane: transient 5xx become 529
|
|
1000
1001
|
* with a retry hint, and a proxy-side replay refusal keeps its no-retry marking.
|
|
1001
1002
|
*/
|
|
1002
|
-
function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog): Response {
|
|
1003
|
+
function nativeMessagesErrorResponse(response: Response, bodyText: string, finishLog: FinishLog, providerName: string, provider: OcxProviderConfig): Response {
|
|
1003
1004
|
let upstreamType: string | undefined;
|
|
1004
1005
|
let upstreamCode: string | undefined;
|
|
1005
1006
|
let upstreamMessage: string | undefined;
|
|
@@ -1013,8 +1014,20 @@ function nativeMessagesErrorResponse(response: Response, bodyText: string, finis
|
|
|
1013
1014
|
}
|
|
1014
1015
|
} catch { /* keep generic classification */ }
|
|
1015
1016
|
const detail = formatAnthropicErrorBody(response.status, response.headers, bodyText);
|
|
1016
|
-
const
|
|
1017
|
+
const rawMessage = upstreamMessage
|
|
1017
1018
|
?? (detail ? `Provider error ${response.status}: ${detail}` : `Provider error ${response.status}`);
|
|
1019
|
+
const message = enrichOpenCodeZenUpstreamMessage(rawMessage, {
|
|
1020
|
+
status: response.status,
|
|
1021
|
+
providerName,
|
|
1022
|
+
baseUrl: provider.baseUrl,
|
|
1023
|
+
adapter: provider.adapter,
|
|
1024
|
+
authMode: provider.authMode,
|
|
1025
|
+
hasApiKey: Boolean(provider.apiKey?.trim()),
|
|
1026
|
+
upstreamRetryAfter: response.headers.get("retry-after"),
|
|
1027
|
+
upstreamErrorType: upstreamType,
|
|
1028
|
+
upstreamErrorCode: upstreamCode,
|
|
1029
|
+
supportsHttpSameKeyRetry: true,
|
|
1030
|
+
});
|
|
1018
1031
|
const classified = classifyError(
|
|
1019
1032
|
response.status,
|
|
1020
1033
|
upstreamType ?? (response.status === 401 ? "authentication_error"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindAnthropicRejectionResponse } from "../../adapters/anthropic-rejection-memo";
|
|
1
2
|
import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
|
|
2
3
|
import type { ResponsesRequestContext } from "./core-options";
|
|
3
4
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
applyUpstreamRecoveryInit,
|
|
30
31
|
isNonReplayableResponse,
|
|
31
32
|
prepareSameTarget429Wait,
|
|
33
|
+
SendBudgetExhaustedError,
|
|
32
34
|
} from "../../lib/upstream-retry";
|
|
33
35
|
import { unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
|
|
34
36
|
import { AdmissionModelDeniedError, MODEL_NOT_ALLOWED_FOR_KEY } from "../admission-model-scope";
|
|
@@ -121,6 +123,7 @@ export function createAdapterContinuations(
|
|
|
121
123
|
| "rateLimitRetries"
|
|
122
124
|
| "stallTimeoutMs"
|
|
123
125
|
| "localUpstream"
|
|
126
|
+
| "thinkingSignatureRecovery"
|
|
124
127
|
>,
|
|
125
128
|
) {
|
|
126
129
|
const { options, logCtx, config } = requestContext;
|
|
@@ -159,6 +162,10 @@ export function createAdapterContinuations(
|
|
|
159
162
|
errorType: MODEL_NOT_ALLOWED_FOR_KEY, code: MODEL_NOT_ALLOWED_FOR_KEY, retryable: false,
|
|
160
163
|
};
|
|
161
164
|
}
|
|
165
|
+
if (refusal instanceof SendBudgetExhaustedError) return {
|
|
166
|
+
type: "error", status: 429, errorType: "rate_limit_error", code: refusal.code,
|
|
167
|
+
message: refusal.message, retryable: false,
|
|
168
|
+
};
|
|
162
169
|
return {
|
|
163
170
|
type: "error",
|
|
164
171
|
message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}`,
|
|
@@ -309,6 +316,14 @@ export function createAdapterContinuations(
|
|
|
309
316
|
const recoveryKind = nextContinuationRecoveryKind;
|
|
310
317
|
nextContinuationRecoveryKind = undefined;
|
|
311
318
|
response = await fetchContinuation(recoveryKind);
|
|
319
|
+
bindAnthropicRejectionResponse(transportState.sameTargetRequest, response);
|
|
320
|
+
if (initialRecoveryKind === "empty-completion" && await adapterExchange.thinkingSignatureRecovery.prepareHttp(
|
|
321
|
+
response, transportState.activeAdapter.name, nextParsed,
|
|
322
|
+
)) {
|
|
323
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
324
|
+
invalidateSameTargetRequest();
|
|
325
|
+
response = await fetchContinuation("opaque-blob-rejection");
|
|
326
|
+
}
|
|
312
327
|
if (kiroRefusalPendingReplay) {
|
|
313
328
|
try { void kiroRefusalPendingReplay.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
314
329
|
kiroRefusalPendingReplay = undefined;
|
|
@@ -704,24 +719,25 @@ export function createAdapterContinuations(
|
|
|
704
719
|
// listener that can cancel its reader even while the parser holds the body lock.
|
|
705
720
|
// The shared signal is not aborted on timeout: this generator must still emit
|
|
706
721
|
// the error that the already-live Responses bridge turns into a terminal.
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
bodyInactivityMs,
|
|
712
|
-
|
|
713
|
-
)
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
yield
|
|
724
|
-
}
|
|
722
|
+
const parse = async function* (current: Response): AsyncGenerator<AdapterEvent> {
|
|
723
|
+
if (!current.ok) {
|
|
724
|
+
yield await adapterExchange.thinkingSignatureRecovery.projectHttpFailure(current, upstream.signal);
|
|
725
|
+
} else if (nextParsed.stream) {
|
|
726
|
+
yield* readResponseStreamWithInactivity(current, upstream.signal, bodyInactivityMs,
|
|
727
|
+
guarded => transportState.activeAdapter.parseStream(guarded, translatorBudget, logCtx.activeTierMetadata));
|
|
728
|
+
} else if (transportState.activeAdapter.parseResponse) {
|
|
729
|
+
yield* await readResponseBodyWithInactivity(current, upstream.signal, bodyInactivityMs,
|
|
730
|
+
guarded => transportState.activeAdapter.parseResponse!(guarded, translatorBudget, logCtx.activeTierMetadata));
|
|
731
|
+
} else {
|
|
732
|
+
try { void current.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
733
|
+
yield { type: "error", message: "Provider continuation does not support response parsing" };
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
yield* adapterExchange.thinkingSignatureRecovery.wrap(parse(response), async function* () {
|
|
737
|
+
invalidateSameTargetRequest();
|
|
738
|
+
yield* parse(await fetchContinuation("opaque-blob-rejection"));
|
|
739
|
+
}, () => transportState.activeAdapter.name,
|
|
740
|
+
initialRecoveryKind === "empty-completion" && !isNonReplayableResponse(response), response, nextParsed);
|
|
725
741
|
} catch (error) {
|
|
726
742
|
// Classify the thrown error before reading signal state: cancelling the stalled
|
|
727
743
|
// source can synchronously abort the shared signal, which would otherwise report
|
|
@@ -23,6 +23,9 @@ import { resolveStallTimeoutMs } from "../../stall-timeout";
|
|
|
23
23
|
import { clientEncoderForDelivery, deliverClientEncodedResponse } from "../inference/client-encoder-delivery";
|
|
24
24
|
import { noteKiroServedSuccess } from "../../providers/kiro-usage";
|
|
25
25
|
import { persistKiroAccountState } from "../../providers/kiro-account-state-disk";
|
|
26
|
+
import { readDisplaySafeErrorText, normalizeUpstreamErrorText } from "./core-errors";
|
|
27
|
+
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
28
|
+
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
26
29
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
27
30
|
import { guardEmptyToolCatalogEvents, guardEmptyToolCatalogStream } from "../../lib/empty-tool-catalog";
|
|
28
31
|
|
|
@@ -49,7 +52,7 @@ export async function deliverAdapterResponse(
|
|
|
49
52
|
| "notifyResponseComplete"
|
|
50
53
|
>,
|
|
51
54
|
completionPolicy: Pick<ResponsesCompletionPolicy, "emptyCompletionGuardEnabled">,
|
|
52
|
-
adapterExchange: Pick<AdapterExchange, "upstreamResponse" | "upstream" | "cleanupUpstreamAbort" | "localUpstream">,
|
|
55
|
+
adapterExchange: Pick<AdapterExchange, "upstreamResponse" | "upstream" | "cleanupUpstreamAbort" | "localUpstream" | "thinkingSignatureRecovery" | "refetchThinkingSignature">,
|
|
53
56
|
continuationState: Pick<AdapterContinuations, "terminalGuardEnabled" | "fetchTerminalGuardContinuation" | "fetchGuardedEmptyCompletionRetry">,
|
|
54
57
|
): Promise<Response> {
|
|
55
58
|
const { logCtx, options, config } = requestContext;
|
|
@@ -61,7 +64,7 @@ export async function deliverAdapterResponse(
|
|
|
61
64
|
rememberKiroDeliveredFinalAnswer,
|
|
62
65
|
responseStateOptions,
|
|
63
66
|
} = requestState;
|
|
64
|
-
const { upstreamResponse, upstream, cleanupUpstreamAbort, localUpstream } = adapterExchange;
|
|
67
|
+
const { upstreamResponse, upstream, cleanupUpstreamAbort, localUpstream, thinkingSignatureRecovery, refetchThinkingSignature } = adapterExchange;
|
|
65
68
|
const {
|
|
66
69
|
terminalGuardEnabled,
|
|
67
70
|
fetchTerminalGuardContinuation,
|
|
@@ -77,6 +80,43 @@ export async function deliverAdapterResponse(
|
|
|
77
80
|
const { routedCompaction } = sidecarState;
|
|
78
81
|
const bodyInactivityMs = resolveStallTimeoutMs(config.stallTimeoutSec, { localUpstream });
|
|
79
82
|
const upstreamRequestsStream = parsed.stream || isCanonicalOpenAiForwardProvider(route.provider);
|
|
83
|
+
const recoverThinkingSignature = (events: AsyncIterable<AdapterEvent>): AsyncIterable<AdapterEvent> =>
|
|
84
|
+
thinkingSignatureRecovery.wrap(events, async function* () {
|
|
85
|
+
const result = await refetchThinkingSignature();
|
|
86
|
+
let response = "failed" in result ? result.failed : result;
|
|
87
|
+
// A replacement can be an HTTP/auth/budget refusal; never feed its JSON body to an SSE parser.
|
|
88
|
+
if (!response.ok) {
|
|
89
|
+
// A local refusal closes the upstream signal before returning its generated JSON.
|
|
90
|
+
// Reading that bounded local body still honors caller cancellation.
|
|
91
|
+
const errorSignal = "failed" in result ? options.abortSignal ?? new AbortController().signal : upstream.signal;
|
|
92
|
+
const nonReplayable = isNonReplayableResponse(response);
|
|
93
|
+
if (nonReplayable) response = await sanitizeNonReplayableUpstreamError(response, errorSignal);
|
|
94
|
+
const body = await readDisplaySafeErrorText(response, errorSignal, "Upstream recovery failed");
|
|
95
|
+
const error = normalizeUpstreamErrorText(body, "Upstream recovery failed");
|
|
96
|
+
yield { type: "error", status: response.status, message: error.message ?? error.safeText,
|
|
97
|
+
errorType: error.type ?? "upstream_error", ...(error.code ? { code: error.code } : {}),
|
|
98
|
+
...(nonReplayable ? { retryable: false } : {}) };
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const retryEvents = upstreamRequestsStream
|
|
103
|
+
? readResponseStreamWithInactivity(response, upstream.signal, bodyInactivityMs,
|
|
104
|
+
guarded => transportState.activeAdapter.parseStream(guarded, translatorBudget, logCtx.activeTierMetadata))
|
|
105
|
+
: (async function* () {
|
|
106
|
+
yield* await readResponseBodyWithInactivity(response, upstream.signal, bodyInactivityMs,
|
|
107
|
+
guarded => transportState.activeAdapter.parseResponse!(guarded, translatorBudget, logCtx.activeTierMetadata));
|
|
108
|
+
})();
|
|
109
|
+
for await (const event of retryEvents) {
|
|
110
|
+
options.onCompactionRecoveryAdapterEvent?.(event);
|
|
111
|
+
yield event;
|
|
112
|
+
}
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (!(error instanceof ResponseBodyInactivityError)) throw error;
|
|
115
|
+
yield { type: "error", message: "Upstream recovery response body stalled before completing",
|
|
116
|
+
status: 504, errorType: "upstream_error" };
|
|
117
|
+
}
|
|
118
|
+
}, () => transportState.activeAdapter.name, !isNonReplayableResponse(upstreamResponse), upstreamResponse);
|
|
119
|
+
|
|
80
120
|
const clientToolCatalog = requestState.clientToolCatalogRestriction && route.provider.authMode !== "forward"
|
|
81
121
|
? requestState.clientToolCatalogRestriction : undefined;
|
|
82
122
|
const deliveryToolBridgeMaps = clientToolCatalog
|
|
@@ -110,15 +150,16 @@ export async function deliverAdapterResponse(
|
|
|
110
150
|
throw error;
|
|
111
151
|
}
|
|
112
152
|
})();
|
|
153
|
+
const recoveredEvents = recoverThinkingSignature(initialEventStream);
|
|
113
154
|
const eventStream = terminalGuardEnabled
|
|
114
155
|
? guardTerminalEventStream({
|
|
115
156
|
parsed,
|
|
116
|
-
firstEvents:
|
|
157
|
+
firstEvents: recoveredEvents,
|
|
117
158
|
adapterName: transportState.activeAdapter.name,
|
|
118
159
|
maxAutoContinuations: 1,
|
|
119
160
|
continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
|
|
120
161
|
})
|
|
121
|
-
:
|
|
162
|
+
: recoveredEvents;
|
|
122
163
|
// Optional advisor guard (registered per request by the sidecar planner; absent unless the
|
|
123
164
|
// user enabled the advisor): holds synthetic `advisor` tool calls, consults the configured
|
|
124
165
|
// expert model, and re-dispatches the worker. Sits inside the empty-completion guard so an
|
|
@@ -147,6 +188,7 @@ export async function deliverAdapterResponse(
|
|
|
147
188
|
// One completion owner for both deliveries: the bridge calls it from its terminal, the
|
|
148
189
|
// direct client encoder from the fold of the same events.
|
|
149
190
|
const onCompletedResponse = (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => {
|
|
191
|
+
if (!options.abortSignal?.aborted) thinkingSignatureRecovery.rememberSuccess(response);
|
|
150
192
|
const served = transportState.sentOAuthSnapshot;
|
|
151
193
|
if (transportState.activeAdapter.name === "kiro" && response.status === "completed" && served
|
|
152
194
|
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
@@ -250,18 +292,22 @@ export async function deliverAdapterResponse(
|
|
|
250
292
|
));
|
|
251
293
|
}
|
|
252
294
|
for (const event of initialEvents) options.onCompactionRecoveryAdapterEvent?.(event);
|
|
295
|
+
const recoveredEvents: AdapterEvent[] = [];
|
|
296
|
+
for await (const event of recoverThinkingSignature((async function* () { yield* initialEvents; })())) {
|
|
297
|
+
recoveredEvents.push(event);
|
|
298
|
+
}
|
|
253
299
|
let guardedEvents: AdapterEvent[];
|
|
254
300
|
if (terminalGuardEnabled) {
|
|
255
301
|
guardedEvents = [];
|
|
256
302
|
for await (const event of guardTerminalEventStream({
|
|
257
303
|
parsed,
|
|
258
|
-
firstEvents: (async function* () { yield*
|
|
304
|
+
firstEvents: (async function* () { yield* recoveredEvents; })(),
|
|
259
305
|
adapterName: transportState.activeAdapter.name,
|
|
260
306
|
maxAutoContinuations: 1,
|
|
261
307
|
continuation: next => fetchTerminalGuardContinuation(next, undefined, !parsed.stream),
|
|
262
308
|
})) guardedEvents.push(event);
|
|
263
309
|
} else {
|
|
264
|
-
guardedEvents =
|
|
310
|
+
guardedEvents = recoveredEvents;
|
|
265
311
|
}
|
|
266
312
|
// Optional advisor guard — same semantics as the streaming branch above.
|
|
267
313
|
if (parsed._advisorGuard) {
|
|
@@ -325,6 +371,7 @@ export async function deliverAdapterResponse(
|
|
|
325
371
|
// #1926 gap 2: same buffered-path durability bound as the primary branch.
|
|
326
372
|
await awaitThoughtSignatureDurability();
|
|
327
373
|
if (adapterResponseReachedServingTerminal(events, json)) {
|
|
374
|
+
if (!options.abortSignal?.aborted) thinkingSignatureRecovery.rememberSuccess(json);
|
|
328
375
|
const served = transportState.sentOAuthSnapshot;
|
|
329
376
|
if (transportState.activeAdapter.name === "kiro" && json.status === "completed" && served
|
|
330
377
|
&& noteKiroServedSuccess(served.accountId, served.generation)) persistKiroAccountState();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindAnthropicRejectionResponse } from "../../adapters/anthropic-rejection-memo";
|
|
1
2
|
import { deferPhysicalSendReporter } from "./deferred-send-reporter";
|
|
2
3
|
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
3
4
|
import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
|
|
@@ -87,6 +88,7 @@ import {
|
|
|
87
88
|
reasoningEffortRejectionText,
|
|
88
89
|
anthropicFastRefused,
|
|
89
90
|
} from "./core-opaque-recovery";
|
|
91
|
+
import { createAnthropicThinkingRecovery } from "./anthropic-thinking-recovery";
|
|
90
92
|
import { shouldAttemptImageTierRetry } from "../image-retry";
|
|
91
93
|
import {
|
|
92
94
|
isTransientConsoleGoUploadRejection,
|
|
@@ -591,7 +593,7 @@ export async function prepareAdapterExchange(
|
|
|
591
593
|
let rateLimitRetries = 0;
|
|
592
594
|
// Shared with the terminal-guard continuation below: an image-tier reduction that let the
|
|
593
595
|
// main request clear a 413 must not be forgotten on the very next continuation build.
|
|
594
|
-
|
|
596
|
+
// Recovery helpers also serve initial HTTP-200 adapter errors during delivery.
|
|
595
597
|
// Recovery loop: multi-key 429 failover + at most ONE opaque-state rebuild and ONE
|
|
596
598
|
// anthropic 413 tightened retry
|
|
597
599
|
// (devlog/260714_image_normalization_pipeline/030). One mutable activeAdapter serves
|
|
@@ -600,6 +602,9 @@ export async function prepareAdapterExchange(
|
|
|
600
602
|
// 413→429 rotation cannot silently undo the tightening.
|
|
601
603
|
let imageRetryAttempted = false;
|
|
602
604
|
const opaqueBlobRecoveryGuard: OpaqueBlobRecoveryGuard = { attempted: false };
|
|
605
|
+
const thinkingSignatureRecovery = createAnthropicThinkingRecovery(
|
|
606
|
+
parsed, upstream.signal, opaqueBlobRecoveryGuard, () => !sendBudgetExhausted(),
|
|
607
|
+
);
|
|
603
608
|
// Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
|
|
604
609
|
// moments later; at most one byte-identical replay is allowed per request.
|
|
605
610
|
const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
|
|
@@ -863,6 +868,11 @@ export async function prepareAdapterExchange(
|
|
|
863
868
|
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
864
869
|
}
|
|
865
870
|
};
|
|
871
|
+
const refetchThinkingSignature = () => {
|
|
872
|
+
invalidateSameTargetRequest();
|
|
873
|
+
return rebuildAndRefetch("opaque-blob-rejection");
|
|
874
|
+
};
|
|
875
|
+
if (!upstreamResponse.ok) {
|
|
866
876
|
const rotateAntigravityAuth = async (
|
|
867
877
|
failedResponse: Response,
|
|
868
878
|
recovery: AttemptRecoveryKind,
|
|
@@ -920,6 +930,17 @@ export async function prepareAdapterExchange(
|
|
|
920
930
|
return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
|
|
921
931
|
} finally { cleanupUpstreamAbort(); }
|
|
922
932
|
}
|
|
933
|
+
bindAnthropicRejectionResponse(transportState.sameTargetRequest, upstreamResponse);
|
|
934
|
+
if (await thinkingSignatureRecovery.prepareHttp(
|
|
935
|
+
upstreamResponse, transportState.activeAdapter.name,
|
|
936
|
+
)) {
|
|
937
|
+
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* closed */ }
|
|
938
|
+
invalidateSameTargetRequest();
|
|
939
|
+
const result = await rebuildAndRefetch("opaque-blob-rejection");
|
|
940
|
+
if ("failed" in result) return result.failed;
|
|
941
|
+
upstreamResponse = result;
|
|
942
|
+
continue recovery;
|
|
943
|
+
}
|
|
923
944
|
if (
|
|
924
945
|
upstreamResponse.status === 401
|
|
925
946
|
&& isOAuth401ReplayProvider
|
|
@@ -1877,6 +1898,8 @@ export async function prepareAdapterExchange(
|
|
|
1877
1898
|
authMode: route.provider.authMode,
|
|
1878
1899
|
hasApiKey: Boolean(route.provider.apiKey?.trim()),
|
|
1879
1900
|
upstreamRetryAfter,
|
|
1901
|
+
upstreamErrorType: normalized.type,
|
|
1902
|
+
upstreamErrorCode: normalized.code,
|
|
1880
1903
|
// This recovery path is the HTTP Responses wire; custom runTurn transports
|
|
1881
1904
|
// never reach enrichOpenCodeZenUpstreamMessage here.
|
|
1882
1905
|
supportsHttpSameKeyRetry: true,
|
|
@@ -1902,6 +1925,7 @@ export async function prepareAdapterExchange(
|
|
|
1902
1925
|
}
|
|
1903
1926
|
}
|
|
1904
1927
|
|
|
1928
|
+
bindAnthropicRejectionResponse(transportState.sameTargetRequest, upstreamResponse);
|
|
1905
1929
|
cancelBodyOnAbort(upstreamResponse.body, upstream.signal);
|
|
1906
1930
|
|
|
1907
1931
|
return {
|
|
@@ -1911,6 +1935,8 @@ export async function prepareAdapterExchange(
|
|
|
1911
1935
|
stallTimeoutMs,
|
|
1912
1936
|
localUpstream,
|
|
1913
1937
|
upstreamResponse,
|
|
1938
|
+
thinkingSignatureRecovery,
|
|
1939
|
+
refetchThinkingSignature,
|
|
1914
1940
|
rateLimitPolicy,
|
|
1915
1941
|
get rateLimitRetries(): typeof rateLimitRetries {
|
|
1916
1942
|
return rateLimitRetries;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { anthropicRejectionPolicy, bindAnthropicRejectionPolicy, rejectedAnthropicPayloads } from "../../adapters/anthropic-rejection-memo";
|
|
2
|
+
import { normalizeUpstreamErrorText, readDisplaySafeErrorText } from "./core-errors";
|
|
3
|
+
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
4
|
+
import { isLikelyRealAnthropicThinkingSignature } from "../../adapters/anthropic";
|
|
5
|
+
import type { AdapterEvent, OcxParsedRequest } from "../../types";
|
|
6
|
+
import { upstreamErrorMessageFromPayload } from "../../lib/errors";
|
|
7
|
+
import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
8
|
+
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
9
|
+
import { rememberReasoningReplayOpaqueBlobRejection } from "../../responses/reasoning-replay-cache";
|
|
10
|
+
import { prepareOpaqueBlobRecovery, type OpaqueBlobRecoveryGuard } from "./core-opaque-recovery";
|
|
11
|
+
|
|
12
|
+
/** Only the explicit conversation-binding refusal authorizes removing signed history. */
|
|
13
|
+
export function isAnthropicThinkingSignatureBindingRejectionMessage(message: string): boolean {
|
|
14
|
+
return message.includes("Invalid `signature` in `thinking` block")
|
|
15
|
+
&& message.includes("The block is bound to a different conversation");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function carriesThinkingState(parsed: OcxParsedRequest): boolean {
|
|
19
|
+
return parsed.context.messages.some(message => message.role === "assistant"
|
|
20
|
+
&& message.content.some(part => part.type === "thinking"
|
|
21
|
+
&& (isLikelyRealAnthropicThinkingSignature(part.signature)
|
|
22
|
+
|| part.redacted?.some(data => typeof data === "string" && data.length > 0))));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** One request owns both the HTTP refusal and initial-event recovery, never a fresh retry guard. */
|
|
26
|
+
export function createAnthropicThinkingRecovery(
|
|
27
|
+
parsed: OcxParsedRequest,
|
|
28
|
+
signal: AbortSignal,
|
|
29
|
+
guard: OpaqueBlobRecoveryGuard,
|
|
30
|
+
canRetry: () => boolean,
|
|
31
|
+
) {
|
|
32
|
+
let rejectedScope: OcxParsedRequest["_reasoningReplayScope"];
|
|
33
|
+
let rejectedPayloads: ReadonlySet<string> | undefined;
|
|
34
|
+
let armed = false;
|
|
35
|
+
let outputSeen = false;
|
|
36
|
+
let completed = false;
|
|
37
|
+
let failed = false;
|
|
38
|
+
let remembered = false;
|
|
39
|
+
|
|
40
|
+
const prepare = (message: string, adapterName: string, attempt = parsed, response?: Response): boolean => {
|
|
41
|
+
if (signal.aborted || !canRetry() || adapterName !== "anthropic" || guard.attempted
|
|
42
|
+
|| parsed._stripReasoningEncryptedContent === true || !carriesThinkingState(attempt)
|
|
43
|
+
|| !isAnthropicThinkingSignatureBindingRejectionMessage(message)) return false;
|
|
44
|
+
rejectedPayloads = rejectedAnthropicPayloads(response);
|
|
45
|
+
// A real send must prove that it serialized opaque history; otherwise rebuilding could
|
|
46
|
+
// repeat the same request. Direct event-policy callers have no HTTP body to identify.
|
|
47
|
+
if (response && !rejectedPayloads?.size) return false;
|
|
48
|
+
const previous = anthropicRejectionPolicy(parsed);
|
|
49
|
+
if (rejectedPayloads && previous?.mode === "selective") {
|
|
50
|
+
const merged = new Set(previous.fingerprints);
|
|
51
|
+
for (const payload of rejectedPayloads) merged.add(payload);
|
|
52
|
+
rejectedPayloads = merged.size <= 256 ? merged : undefined;
|
|
53
|
+
}
|
|
54
|
+
guard.attempted = true;
|
|
55
|
+
armed = true;
|
|
56
|
+
const scope = parsed._reasoningReplayScope;
|
|
57
|
+
rejectedScope = scope ? { ...scope, current: scope.current ? { ...scope.current } : undefined } : undefined;
|
|
58
|
+
if (!rejectedPayloads) {
|
|
59
|
+
prepareOpaqueBlobRecovery(parsed);
|
|
60
|
+
if (attempt !== parsed) prepareOpaqueBlobRecovery(attempt);
|
|
61
|
+
}
|
|
62
|
+
const policy = rejectedPayloads ? { mode: "selective" as const, fingerprints: rejectedPayloads } : { mode: "blanket" as const };
|
|
63
|
+
bindAnthropicRejectionPolicy(parsed, policy);
|
|
64
|
+
bindAnthropicRejectionPolicy(attempt, policy);
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const prepareHttp = async (response: Response, adapterName: string, attempt = parsed): Promise<boolean> => {
|
|
69
|
+
if (signal.aborted || !canRetry() || response.status !== 400 || isNonReplayableResponse(response)
|
|
70
|
+
|| adapterName !== "anthropic" || guard.attempted
|
|
71
|
+
|| parsed._stripReasoningEncryptedContent === true || !carriesThinkingState(attempt)) return false;
|
|
72
|
+
try {
|
|
73
|
+
const body = await readBoundedResponseBody(response.clone(), { signal });
|
|
74
|
+
if (!body.displaySafe || body.truncated) return false;
|
|
75
|
+
const message = upstreamErrorMessageFromPayload(JSON.parse(body.text) as unknown);
|
|
76
|
+
return typeof message === "string" && prepare(message, adapterName, attempt, response);
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const observe = (event: AdapterEvent): void => {
|
|
83
|
+
if (!armed) return;
|
|
84
|
+
if (event.type === "error" || event.type === "incomplete") failed = true;
|
|
85
|
+
if (event.type === "done") completed = true;
|
|
86
|
+
if ((event.type === "text_delta" && event.text.length > 0) || event.type === "tool_call_end") outputSeen = true;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
async function* wrap(
|
|
90
|
+
events: AsyncIterable<AdapterEvent>,
|
|
91
|
+
retry: () => AsyncIterable<AdapterEvent>,
|
|
92
|
+
adapterName: () => string,
|
|
93
|
+
replayAllowed = true,
|
|
94
|
+
response?: Response,
|
|
95
|
+
attempt = parsed,
|
|
96
|
+
): AsyncGenerator<AdapterEvent> {
|
|
97
|
+
let source = events;
|
|
98
|
+
let replaySafe = replayAllowed;
|
|
99
|
+
while (true) {
|
|
100
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
101
|
+
let recovered = false;
|
|
102
|
+
try {
|
|
103
|
+
while (true) {
|
|
104
|
+
const next = await iterator.next();
|
|
105
|
+
if (next.done) return;
|
|
106
|
+
const event = next.value;
|
|
107
|
+
if (replaySafe && event.type === "error" && prepare(event.message, adapterName(), attempt, response)) {
|
|
108
|
+
// Close the rejected body before acquiring another dispatch/pacing slot.
|
|
109
|
+
await iterator.return?.();
|
|
110
|
+
source = retry();
|
|
111
|
+
recovered = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
// Even hidden reasoning, boundaries, and tool activity fence a replay. A heartbeat
|
|
115
|
+
// carrying a side-effect latch is never evidence that a turn is safe to repeat.
|
|
116
|
+
replaySafe &&= event.type === "heartbeat" && event.replayUnsafe !== true;
|
|
117
|
+
observe(event);
|
|
118
|
+
yield event;
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
if (!recovered) await iterator.return?.();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const rememberSuccess = (response: Readonly<Record<string, unknown>>): void => {
|
|
127
|
+
// HTTP 200, EOF, or a guard-generated completion alone cannot prove the stripped send worked.
|
|
128
|
+
// The delivery owner calls this only after its bridge/fold validated a successful terminal.
|
|
129
|
+
if (!armed || remembered || signal.aborted || failed || !completed || !outputSeen
|
|
130
|
+
|| response.status !== "completed" || !Array.isArray(response.output) || response.output.length === 0
|
|
131
|
+
|| !rejectedScope
|
|
132
|
+
|| rejectedScope.clientThreadId !== parsed._reasoningReplayScope?.clientThreadId
|
|
133
|
+
|| JSON.stringify(rejectedScope.current) !== JSON.stringify(parsed._reasoningReplayScope?.current)) return;
|
|
134
|
+
remembered = true;
|
|
135
|
+
rememberReasoningReplayOpaqueBlobRejection(rejectedScope, rejectedPayloads);
|
|
136
|
+
};
|
|
137
|
+
// The search owner supplies only a fully validated current iteration, never search-card output.
|
|
138
|
+
const observeIteration = (events: readonly AdapterEvent[]): void => {
|
|
139
|
+
outputSeen = false;
|
|
140
|
+
completed = false;
|
|
141
|
+
for (const event of events) observe(event);
|
|
142
|
+
};
|
|
143
|
+
const projectHttpFailure = async (response: Response, abortSignal: AbortSignal): Promise<Extract<AdapterEvent, { type: "error" }>> => {
|
|
144
|
+
const nonReplayable = isNonReplayableResponse(response);
|
|
145
|
+
if (nonReplayable) response = await sanitizeNonReplayableUpstreamError(response, abortSignal);
|
|
146
|
+
const text = await readDisplaySafeErrorText(response, abortSignal, `Provider error ${response.status}`);
|
|
147
|
+
const error = normalizeUpstreamErrorText(text, `Provider error ${response.status}`);
|
|
148
|
+
return { type: "error", status: response.status, message: error.message ?? error.safeText,
|
|
149
|
+
errorType: error.type ?? "upstream_error", ...(error.code ? { code: error.code } : {}),
|
|
150
|
+
...(nonReplayable ? { retryable: false } : {}) };
|
|
151
|
+
};
|
|
152
|
+
return { prepareHttp, prepareEvent: prepare, observeIteration, projectHttpFailure, wrap, rememberSuccess };
|
|
153
|
+
}
|
|
@@ -4,6 +4,7 @@ import { comboFailureDecision } from "../../combos";
|
|
|
4
4
|
import { httpStatusFromTerminalError } from "../../lib/errors";
|
|
5
5
|
import type { RequestFailureStage } from "../../lib/request-failure-model";
|
|
6
6
|
import type { RequestLogContext } from "../request-log";
|
|
7
|
+
import { carryResponseToolOwnership } from "./response-tool-ownership";
|
|
7
8
|
import { createSseInspector } from "../relay";
|
|
8
9
|
import { MAX_CLIENT_SSE_FRAME_BYTES } from "../sse-frame-buffer";
|
|
9
10
|
|
|
@@ -198,11 +199,11 @@ function replayBufferedResponse(
|
|
|
198
199
|
reader.cancel(reason).catch(() => undefined);
|
|
199
200
|
},
|
|
200
201
|
});
|
|
201
|
-
return new Response(body, {
|
|
202
|
+
return carryResponseToolOwnership(response, new Response(body, {
|
|
202
203
|
status: response.status,
|
|
203
204
|
statusText: response.statusText,
|
|
204
205
|
headers: response.headers,
|
|
205
|
-
});
|
|
206
|
+
}));
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
function failedTerminalResponse(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AdapterEvent, OcxConfig } from "../../types";
|
|
2
|
+
import { carryResponseToolOwnership } from "./response-tool-ownership";
|
|
2
3
|
import { routeConcreteModel, type RouteResult } from "../../router";
|
|
3
4
|
import { copyPlainData } from "../../lib/plain-data";
|
|
4
5
|
import { jsonUtf8Bytes } from "../../lib/json-byte-size";
|
|
@@ -78,6 +79,7 @@ async function bufferedJson(response: Response, signal: AbortSignal): Promise<{
|
|
|
78
79
|
headers.delete("content-length");
|
|
79
80
|
headers.delete("content-encoding");
|
|
80
81
|
const replacement = new Response(bytes.bytes, { status: response.status, statusText: response.statusText, headers });
|
|
82
|
+
carryResponseToolOwnership(response, replacement);
|
|
81
83
|
if (isNonReplayableResponse(response)) markResponseNonReplayable(replacement);
|
|
82
84
|
try {
|
|
83
85
|
const json: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes.bytes));
|
|
@@ -975,8 +975,8 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
975
975
|
const maxRetrySends = retrySameConfirmedAccount ? 7 : 1;
|
|
976
976
|
let retrySendCount = 0;
|
|
977
977
|
// The gated-model 400 has no roster kind; it stays unattributed as before.
|
|
978
|
-
const moveRecovery: AttemptRecoveryKind | undefined = outcomeStatus === 429
|
|
979
|
-
? "oauth-account-
|
|
978
|
+
const moveRecovery: AttemptRecoveryKind | undefined = outcomeStatus === 429 ? "oauth-account-429"
|
|
979
|
+
: outcomeStatus === 402 ? "oauth-account-402"
|
|
980
980
|
: isTransientUpstreamStatus(outcomeStatus) ? "transient-5xx" : undefined;
|
|
981
981
|
let upstreamResponse: Response;
|
|
982
982
|
try {
|