@bitkyc08/opencodex 2.82.0 → 2.83.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-Ds_qL7o8.js → App-Ba8uG696.js} +4 -4
- package/gui/dist/assets/{Tray-BqHF5Trd.js → Tray-IImbNPRy.js} +1 -1
- package/gui/dist/assets/{index-D3FBCNiP.js → index-BHRZdJuv.js} +12 -12
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-DiJO6sYY.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +18 -14
- package/src/adapters/openai-responses/passthrough.ts +2 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/bridge/errors.ts +7 -1
- package/src/claude/context-windows.ts +3 -3
- package/src/claude/desktop-picker.ts +1 -1
- package/src/cli/account-auth.ts +20 -12
- package/src/cli/resolve.ts +1 -1
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +1 -1
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/catalog/native-models.ts +1 -1
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/generated/compatibility-version.json +91 -75
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +223 -57
- package/src/grok/reset-coupons.ts +13 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/lib/errors.ts +25 -0
- package/src/lib/redact.ts +2 -2
- package/src/oauth/index.ts +1 -1
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/types.ts +1 -1
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-grant-ledger.ts +1 -1
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/registry/entries-core.ts +6 -6
- package/src/providers/registry/entries-extended.ts +4 -0
- package/src/providers/registry/model-seeds.ts +3 -3
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/serve-options.ts +8 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +31 -4
- package/src/server/management/grok-coupon-routes.ts +277 -52
- package/src/server/management/link-routes.ts +42 -20
- package/src/server/management/route-registry.ts +2 -2
- package/src/server/relay-eager.ts +2 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-terminal-status.ts +186 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +65 -6
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-dispatch.ts +115 -60
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-transport.ts +125 -5
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +5 -0
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +1 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CYBER_POLICY_ERROR_CODE,
|
|
3
|
+
httpStatusFromTerminalError,
|
|
4
|
+
isCyberPolicyCode,
|
|
5
|
+
isCyberPolicyMessage,
|
|
6
|
+
isRateLimitOrQuotaFailureMessage,
|
|
7
|
+
recognizedUpstreamError,
|
|
8
|
+
} from "../lib/errors";
|
|
9
|
+
import { redactSecretString } from "../lib/redact";
|
|
10
|
+
|
|
11
|
+
/** First bounded upstream diagnostics shared by request contexts and final rows. */
|
|
12
|
+
export interface UpstreamErrorDiagnostics {
|
|
13
|
+
upstreamErrorCode?: string;
|
|
14
|
+
upstreamErrorType?: string;
|
|
15
|
+
upstreamRequestId?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TerminalStatusContext extends UpstreamErrorDiagnostics {
|
|
19
|
+
terminalHttpStatus?: number;
|
|
20
|
+
terminalErrorCode?: typeof CYBER_POLICY_ERROR_CODE;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UpstreamErrorPayload {
|
|
24
|
+
type?: unknown;
|
|
25
|
+
code?: unknown;
|
|
26
|
+
message?: unknown;
|
|
27
|
+
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
28
|
+
last_error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
29
|
+
response?: {
|
|
30
|
+
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
31
|
+
incomplete_details?: { code?: unknown; message?: unknown; reason?: unknown };
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Bare error evidence is provisional: only a genuine terminal owns account-health status.
|
|
36
|
+
const provisionalStatuses = new WeakMap<TerminalStatusContext, number>();
|
|
37
|
+
const genuineTerminals = new WeakSet<TerminalStatusContext>();
|
|
38
|
+
|
|
39
|
+
const UPSTREAM_DIAGNOSTIC_TOKEN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Upstream diagnostics are provider-controlled. The token alphabet alone also admits
|
|
43
|
+
* credential formats (an echoed `sk-...` key fits it), so a value the shared redactor
|
|
44
|
+
* would change is dropped rather than recorded, warned, or projected into log rows.
|
|
45
|
+
*/
|
|
46
|
+
function isSafeDiagnosticToken(value: unknown): value is string {
|
|
47
|
+
return typeof value === "string" && UPSTREAM_DIAGNOSTIC_TOKEN.test(value) && redactSecretString(value) === value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error classes published by the supported provider APIs (OpenAI/Responses, Anthropic, Gemini).
|
|
52
|
+
* The type is diagnostic only, so the vocabulary is closed: a gateway can echo anything in
|
|
53
|
+
* `error.type`, including a configured credential whose format no redactor recognizes, and a
|
|
54
|
+
* value outside this set is simply not recorded, which is what 2.82.0 did for every type.
|
|
55
|
+
*/
|
|
56
|
+
const KNOWN_UPSTREAM_ERROR_TYPES: ReadonlySet<string> = new Set([
|
|
57
|
+
"api_error", "authentication_error", "billing_error", "insufficient_quota", "invalid_request_error",
|
|
58
|
+
"not_found_error", "overloaded_error", "permission_error", "rate_limit_error", "request_too_large",
|
|
59
|
+
"requests", "server_error", "timeout_error", "tokens",
|
|
60
|
+
"INVALID_ARGUMENT", "FAILED_PRECONDITION", "PERMISSION_DENIED", "UNAUTHENTICATED", "NOT_FOUND",
|
|
61
|
+
"RESOURCE_EXHAUSTED", "INTERNAL", "UNAVAILABLE", "DEADLINE_EXCEEDED",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
/** Bare top-level error codes worth keeping: the classes recognizedUpstreamError maps, plus server_error. */
|
|
65
|
+
const KNOWN_BARE_ERROR_CODES: ReadonlySet<string> = new Set([
|
|
66
|
+
"rate_limit_exceeded", "rate_limit_error", "server_is_overloaded", "overloaded_error", "server_error",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
function noteBoundedDiagnostic(
|
|
70
|
+
logCtx: TerminalStatusContext,
|
|
71
|
+
field: "upstreamErrorCode" | "upstreamErrorType" | "upstreamRequestId",
|
|
72
|
+
value: unknown,
|
|
73
|
+
): void {
|
|
74
|
+
if (logCtx[field] !== undefined) return;
|
|
75
|
+
if (!isSafeDiagnosticToken(value)) return;
|
|
76
|
+
if (field === "upstreamErrorType" && !KNOWN_UPSTREAM_ERROR_TYPES.has(value)) return;
|
|
77
|
+
logCtx[field] = value;
|
|
78
|
+
const status = logCtx.terminalHttpStatus;
|
|
79
|
+
if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function noteUpstreamRequestId(logCtx: TerminalStatusContext, headers: Headers): void {
|
|
83
|
+
noteBoundedDiagnostic(logCtx, "upstreamRequestId", headers.get("openai-request-id") ?? headers.get("x-request-id"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function captureUpstreamTerminalDiagnostics(logCtx: TerminalStatusContext, json: UpstreamErrorPayload): void {
|
|
87
|
+
captureTerminalHttpStatus(logCtx, json);
|
|
88
|
+
noteBoundedDiagnostic(logCtx, "upstreamErrorCode",
|
|
89
|
+
json.error?.code ?? json.last_error?.code ?? json.response?.error?.code
|
|
90
|
+
// A bare top-level code (new in 2.83.0) has no envelope contract, so only a known class is kept.
|
|
91
|
+
?? (typeof json.code === "string" && KNOWN_BARE_ERROR_CODES.has(json.code) ? json.code : undefined));
|
|
92
|
+
noteBoundedDiagnostic(logCtx, "upstreamErrorType",
|
|
93
|
+
json.error?.type ?? json.last_error?.type ?? json.response?.error?.type);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function upstreamDiagnosticLogFields(logCtx: UpstreamErrorDiagnostics): UpstreamErrorDiagnostics {
|
|
97
|
+
return {
|
|
98
|
+
...(logCtx.upstreamErrorCode ? { upstreamErrorCode: logCtx.upstreamErrorCode } : {}),
|
|
99
|
+
...(logCtx.upstreamErrorType ? { upstreamErrorType: logCtx.upstreamErrorType } : {}),
|
|
100
|
+
...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function captureTerminalHttpStatus(
|
|
105
|
+
logCtx: TerminalStatusContext,
|
|
106
|
+
json: UpstreamErrorPayload,
|
|
107
|
+
): void {
|
|
108
|
+
const type = json.type;
|
|
109
|
+
const terminal = type === "response.failed" || type === "response.completed" || type === "response.incomplete";
|
|
110
|
+
if (!terminal && type !== "error") return;
|
|
111
|
+
if (terminal && provisionalStatuses.has(logCtx)) {
|
|
112
|
+
const provisional = provisionalStatuses.get(logCtx);
|
|
113
|
+
provisionalStatuses.delete(logCtx);
|
|
114
|
+
if (logCtx.terminalHttpStatus === provisional) {
|
|
115
|
+
delete logCtx.terminalHttpStatus;
|
|
116
|
+
delete logCtx.terminalErrorCode;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (logCtx.terminalHttpStatus !== undefined || (type === "error" && genuineTerminals.has(logCtx))) return;
|
|
120
|
+
if (terminal) genuineTerminals.add(logCtx);
|
|
121
|
+
const noteStatus = (status: number): void => {
|
|
122
|
+
logCtx.terminalHttpStatus = status;
|
|
123
|
+
if (type === "error") provisionalStatuses.set(logCtx, status);
|
|
124
|
+
};
|
|
125
|
+
// A completed terminal only retires provisional bare-error evidence; it records no status.
|
|
126
|
+
if (type === "response.completed") return;
|
|
127
|
+
const responseError = json.response?.error;
|
|
128
|
+
const responseDetails = json.response?.incomplete_details;
|
|
129
|
+
const candidates: Array<{ type?: unknown; code?: unknown; message?: unknown } | undefined> = [
|
|
130
|
+
json.error, json.last_error, responseError, responseDetails, json,
|
|
131
|
+
];
|
|
132
|
+
const policy = candidates.some(candidate => (
|
|
133
|
+
candidate?.code === null || typeof candidate?.code === "string"
|
|
134
|
+
) && isCyberPolicyCode(candidate.code as string | null | undefined))
|
|
135
|
+
|| candidates.some(candidate => (
|
|
136
|
+
typeof candidate?.message === "string"
|
|
137
|
+
&& candidate.message.trim().length > 0
|
|
138
|
+
&& isCyberPolicyMessage(candidate.message)
|
|
139
|
+
));
|
|
140
|
+
if (policy) {
|
|
141
|
+
logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
|
|
142
|
+
noteStatus(400);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// A quota terminal can carry only a structured reason, without an error message.
|
|
146
|
+
// Keep this separate from normal output limits and from the policy precedence above.
|
|
147
|
+
const quotaTag = (value: unknown): boolean => value === "usage_limit_reached"
|
|
148
|
+
|| value === "rate_limit_exceeded" || value === "insufficient_quota";
|
|
149
|
+
const structuredRefusal = candidates.some(candidate => [400, 401, 403, 499].includes(
|
|
150
|
+
httpStatusFromTerminalError({
|
|
151
|
+
type: typeof candidate?.type === "string" ? candidate.type : undefined,
|
|
152
|
+
code: typeof candidate?.code === "string" ? candidate.code : undefined,
|
|
153
|
+
}),
|
|
154
|
+
));
|
|
155
|
+
const ordinaryIncompleteReason = typeof responseDetails?.reason === "string"
|
|
156
|
+
&& ["max_output_tokens", "content_filter", "steered", "upstream_stall_timeout", "adapter_eof"].includes(responseDetails.reason);
|
|
157
|
+
if (type === "response.incomplete" && !structuredRefusal && (quotaTag(responseDetails?.reason) || candidates.some(candidate =>
|
|
158
|
+
quotaTag(candidate?.code)
|
|
159
|
+
|| quotaTag(candidate?.type) || candidate?.type === "rate_limit_error"
|
|
160
|
+
|| (!ordinaryIncompleteReason && typeof candidate?.message === "string" && isRateLimitOrQuotaFailureMessage(candidate.message))
|
|
161
|
+
))) {
|
|
162
|
+
// The shared quota classifier also accepts a numeric HTTP status as its message.
|
|
163
|
+
// Preserve explicit payment-required evidence rather than relabeling it as 429.
|
|
164
|
+
noteStatus(candidates.some(candidate => typeof candidate?.message === "string"
|
|
165
|
+
&& Number(candidate.message.trim()) === 402) ? 402 : 429);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (type === "error") {
|
|
169
|
+
const structured = recognizedUpstreamError({
|
|
170
|
+
code: candidates.find(candidate => typeof candidate?.code === "string")?.code,
|
|
171
|
+
type: candidates.slice(0, -1).find(candidate => typeof candidate?.type === "string")?.type,
|
|
172
|
+
});
|
|
173
|
+
// Unrecognized bare errors leave the status to explicit event fields (combo preflight).
|
|
174
|
+
if (structured) noteStatus(structured.httpStatus);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
|
|
178
|
+
const responseCode = responseError.code === null || typeof responseError.code === "string"
|
|
179
|
+
? responseError.code
|
|
180
|
+
: undefined;
|
|
181
|
+
noteStatus(httpStatusFromTerminalError({
|
|
182
|
+
type: typeof responseError.type === "string" ? responseError.type : undefined,
|
|
183
|
+
code: responseCode,
|
|
184
|
+
message: typeof responseError.message === "string" ? responseError.message : undefined,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
@@ -4,6 +4,8 @@ import { stampApiKeyAccountLabel, usesApiKeyAccount } from "../providers/label";
|
|
|
4
4
|
import { KEY_ACCOUNT_LOG_LABEL_RE } from "../codex/account-label";
|
|
5
5
|
import { attemptAccountChanged, sealRequestAttemptIdentity } from "./request-log-account-rotation";
|
|
6
6
|
export { sealRequestAttemptIdentity };
|
|
7
|
+
import { captureUpstreamTerminalDiagnostics, upstreamDiagnosticLogFields, type UpstreamErrorDiagnostics, type UpstreamErrorPayload } from "./request-log-terminal-status";
|
|
8
|
+
export { noteUpstreamRequestId } from "./request-log-terminal-status";
|
|
7
9
|
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
8
10
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
9
11
|
import {
|
|
@@ -12,8 +14,6 @@ import {
|
|
|
12
14
|
httpStatusFromTerminalError as httpStatusFromClassifiedTerminalError,
|
|
13
15
|
isClientClosedMessage,
|
|
14
16
|
isCyberPolicyCode,
|
|
15
|
-
isCyberPolicyMessage,
|
|
16
|
-
isRateLimitOrQuotaFailureMessage,
|
|
17
17
|
isUpstreamResetReplayRefusedMessage,
|
|
18
18
|
upstreamErrorMessageFromPayload,
|
|
19
19
|
} from "../lib/errors";
|
|
@@ -104,7 +104,7 @@ function cacheDiagnosticHooks(): CacheDiagnosticHooks | undefined {
|
|
|
104
104
|
return (globalThis as Record<symbol, CacheDiagnosticHooks | undefined>)[CACHE_DIAGNOSTIC_HOOK];
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
export interface RequestLogContext {
|
|
107
|
+
export interface RequestLogContext extends UpstreamErrorDiagnostics {
|
|
108
108
|
model: string;
|
|
109
109
|
provider: string;
|
|
110
110
|
/** Optional process-lifetime aggregate sink, injected by the server composition owner. */
|
|
@@ -232,8 +232,6 @@ export interface RequestLogContext {
|
|
|
232
232
|
* message) extracted from a `response.failed` SSE payload or non-streaming error body, so the
|
|
233
233
|
* request log / GUI shows the actual upstream failure rather than only the HTTP-mapped code. */
|
|
234
234
|
upstreamError?: string;
|
|
235
|
-
upstreamErrorCode?: string;
|
|
236
|
-
upstreamRequestId?: string;
|
|
237
235
|
/** HTTP status derived from a terminal `response.failed` SSE payload (429/401/503/etc.). */
|
|
238
236
|
terminalHttpStatus?: number;
|
|
239
237
|
/** Recognized structured terminal code whose exact identity must survive status mapping. */
|
|
@@ -256,6 +254,8 @@ export interface RequestLogContext {
|
|
|
256
254
|
conversationStateScrub?: "account-change";
|
|
257
255
|
transportPhase?: "pre_headers" | "mid_stream" | "terminal_sse";
|
|
258
256
|
terminalSource?: "upstream" | "synthetic";
|
|
257
|
+
/** Closed failure evidence supplied by the transport, never inferred from provider text. */
|
|
258
|
+
causeHint?: RequestFailureCause;
|
|
259
259
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
260
260
|
routeDecision?: RouteDecisionTraceV1;
|
|
261
261
|
/** Full eligible provider/model membership from a policy route; never logged. */
|
|
@@ -284,7 +284,7 @@ export function rebindCacheDiagnosticBody(
|
|
|
284
284
|
cacheDiagnosticHooks()?.rebind(body, draft);
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
export interface RequestLogEntry {
|
|
287
|
+
export interface RequestLogEntry extends UpstreamErrorDiagnostics {
|
|
288
288
|
requestId: string;
|
|
289
289
|
/** The logical request this row belongs to (#4546); absent on rows written without a budget. */
|
|
290
290
|
logicalRequestId?: string;
|
|
@@ -344,8 +344,6 @@ export interface RequestLogEntry {
|
|
|
344
344
|
closeReason?: "terminal" | "client_cancel" | "non_stream" | "body_stall" | "body_overflow";
|
|
345
345
|
/** Secret-redacted upstream error reason, surfaced in /api/logs and the GUI detail modal. */
|
|
346
346
|
upstreamError?: string;
|
|
347
|
-
upstreamErrorCode?: string;
|
|
348
|
-
upstreamRequestId?: string;
|
|
349
347
|
usageStatus: UsageStatus;
|
|
350
348
|
usage?: OcxUsage;
|
|
351
349
|
totalTokens?: number;
|
|
@@ -844,6 +842,7 @@ export function requestLogErrorCode(
|
|
|
844
842
|
status: number,
|
|
845
843
|
upstreamError?: string,
|
|
846
844
|
terminalErrorCode?: string,
|
|
845
|
+
replayRefusal = false,
|
|
847
846
|
): string | undefined {
|
|
848
847
|
if (status >= 200 && status < 400) return undefined;
|
|
849
848
|
// A structured terminal code is authoritative even when the provider message is localized,
|
|
@@ -872,10 +871,8 @@ export function requestLogErrorCode(
|
|
|
872
871
|
return "permission_denied";
|
|
873
872
|
}
|
|
874
873
|
if (status === 429) {
|
|
875
|
-
//
|
|
876
|
-
|
|
877
|
-
// distinguishes a proxy refusal from provider throttling.
|
|
878
|
-
if (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError)) {
|
|
874
|
+
// Prefer transport provenance; retain the legacy message fallback for older callers.
|
|
875
|
+
if (replayRefusal || (upstreamError?.trim() && isUpstreamResetReplayRefusedMessage(upstreamError))) {
|
|
879
876
|
return UPSTREAM_RESET_REPLAY_REFUSED_CODE;
|
|
880
877
|
}
|
|
881
878
|
return "rate_limit_exceeded";
|
|
@@ -1099,24 +1096,6 @@ export function inspectResponseLogSsePayloadParsed(
|
|
|
1099
1096
|
* a non-streaming JSON error body. We keep the FIRST non-empty reason (the original failure) and
|
|
1100
1097
|
* run it through redactSecretString so secrets never reach /api/logs. Pure; safe on any text.
|
|
1101
1098
|
*/
|
|
1102
|
-
const UPSTREAM_DIAGNOSTIC_TOKEN = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
1103
|
-
|
|
1104
|
-
function noteBoundedDiagnostic(
|
|
1105
|
-
logCtx: RequestLogContext,
|
|
1106
|
-
field: "upstreamErrorCode" | "upstreamRequestId",
|
|
1107
|
-
value: unknown,
|
|
1108
|
-
): void {
|
|
1109
|
-
if (logCtx[field] !== undefined) return;
|
|
1110
|
-
if (typeof value !== "string" || !UPSTREAM_DIAGNOSTIC_TOKEN.test(value)) return;
|
|
1111
|
-
logCtx[field] = value;
|
|
1112
|
-
const status = logCtx.terminalHttpStatus;
|
|
1113
|
-
if (status === undefined || status >= 500) console.warn(`[opencodex] upstream failure${status ? ` status=${status}` : ""}${logCtx.upstreamErrorCode ? ` code=${logCtx.upstreamErrorCode}` : ""}${logCtx.upstreamRequestId ? ` request_id=${logCtx.upstreamRequestId}` : ""}`);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
export function noteUpstreamRequestId(logCtx: RequestLogContext, headers: Headers): void {
|
|
1117
|
-
noteBoundedDiagnostic(logCtx, "upstreamRequestId", headers.get("openai-request-id") ?? headers.get("x-request-id"));
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
1099
|
function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
|
|
1121
1100
|
if (!text) return;
|
|
1122
1101
|
let parsed: unknown | undefined;
|
|
@@ -1134,21 +1113,8 @@ function captureUpstreamErrorParsed(
|
|
|
1134
1113
|
parsed: unknown | undefined,
|
|
1135
1114
|
): void {
|
|
1136
1115
|
if (parsed !== undefined && parsed !== null) {
|
|
1137
|
-
const json = parsed as
|
|
1138
|
-
|
|
1139
|
-
error?: { message?: unknown; code?: unknown };
|
|
1140
|
-
last_error?: { message?: unknown; code?: unknown };
|
|
1141
|
-
response?: {
|
|
1142
|
-
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
1143
|
-
incomplete_details?: { reason?: unknown; message?: unknown };
|
|
1144
|
-
};
|
|
1145
|
-
};
|
|
1146
|
-
captureTerminalHttpStatus(logCtx, json);
|
|
1147
|
-
noteBoundedDiagnostic(
|
|
1148
|
-
logCtx,
|
|
1149
|
-
"upstreamErrorCode",
|
|
1150
|
-
json.error?.code ?? json.response?.error?.code ?? json.last_error?.code,
|
|
1151
|
-
);
|
|
1116
|
+
const json = parsed as UpstreamErrorPayload;
|
|
1117
|
+
captureUpstreamTerminalDiagnostics(logCtx, json);
|
|
1152
1118
|
const reason = json?.response?.incomplete_details?.reason;
|
|
1153
1119
|
if (json.type === "response.incomplete"
|
|
1154
1120
|
&& logCtx.terminalIncompleteReason === undefined
|
|
@@ -1192,75 +1158,6 @@ function incompleteReasonLabel(reason: string): string {
|
|
|
1192
1158
|
}
|
|
1193
1159
|
}
|
|
1194
1160
|
|
|
1195
|
-
function captureTerminalHttpStatus(
|
|
1196
|
-
logCtx: RequestLogContext,
|
|
1197
|
-
json: {
|
|
1198
|
-
type?: unknown;
|
|
1199
|
-
code?: unknown;
|
|
1200
|
-
message?: unknown;
|
|
1201
|
-
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
1202
|
-
last_error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
1203
|
-
response?: {
|
|
1204
|
-
error?: { type?: unknown; code?: unknown; message?: unknown };
|
|
1205
|
-
incomplete_details?: { code?: unknown; message?: unknown; reason?: unknown };
|
|
1206
|
-
};
|
|
1207
|
-
},
|
|
1208
|
-
): void {
|
|
1209
|
-
if (logCtx.terminalHttpStatus !== undefined) return;
|
|
1210
|
-
const type = json.type;
|
|
1211
|
-
if (type !== "response.failed" && type !== "response.incomplete" && type !== "error") return;
|
|
1212
|
-
const responseError = json.response?.error;
|
|
1213
|
-
const responseDetails = json.response?.incomplete_details;
|
|
1214
|
-
const candidates: Array<{ type?: unknown; code?: unknown; message?: unknown } | undefined> = [
|
|
1215
|
-
json.error, json.last_error, responseError, responseDetails, json,
|
|
1216
|
-
];
|
|
1217
|
-
const policy = candidates.some(candidate => (
|
|
1218
|
-
candidate?.code === null || typeof candidate?.code === "string"
|
|
1219
|
-
) && isCyberPolicyCode(candidate.code as string | null | undefined))
|
|
1220
|
-
|| candidates.some(candidate => (
|
|
1221
|
-
typeof candidate?.message === "string"
|
|
1222
|
-
&& candidate.message.trim().length > 0
|
|
1223
|
-
&& isCyberPolicyMessage(candidate.message)
|
|
1224
|
-
));
|
|
1225
|
-
if (policy) {
|
|
1226
|
-
logCtx.terminalErrorCode = CYBER_POLICY_ERROR_CODE;
|
|
1227
|
-
logCtx.terminalHttpStatus = 400;
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
// A quota terminal can carry only a structured reason, without an error message.
|
|
1231
|
-
// Keep this separate from normal output limits and from the policy precedence above.
|
|
1232
|
-
const quotaTag = (value: unknown): boolean => value === "usage_limit_reached"
|
|
1233
|
-
|| value === "rate_limit_exceeded" || value === "insufficient_quota";
|
|
1234
|
-
const structuredRefusal = candidates.some(candidate => [400, 401, 403, 499].includes(
|
|
1235
|
-
httpStatusFromTerminalError({
|
|
1236
|
-
type: typeof candidate?.type === "string" ? candidate.type : undefined,
|
|
1237
|
-
code: typeof candidate?.code === "string" ? candidate.code : undefined,
|
|
1238
|
-
}),
|
|
1239
|
-
));
|
|
1240
|
-
const ordinaryIncompleteReason = typeof responseDetails?.reason === "string"
|
|
1241
|
-
&& ["max_output_tokens", "content_filter", "steered", "upstream_stall_timeout", "adapter_eof"].includes(responseDetails.reason);
|
|
1242
|
-
if (type === "response.incomplete" && !structuredRefusal && (quotaTag(responseDetails?.reason) || candidates.some(candidate =>
|
|
1243
|
-
quotaTag(candidate?.code)
|
|
1244
|
-
|| quotaTag(candidate?.type) || candidate?.type === "rate_limit_error"
|
|
1245
|
-
|| (!ordinaryIncompleteReason && typeof candidate?.message === "string" && isRateLimitOrQuotaFailureMessage(candidate.message))
|
|
1246
|
-
))) {
|
|
1247
|
-
// The shared quota classifier also accepts a numeric HTTP status as its message.
|
|
1248
|
-
// Preserve explicit payment-required evidence rather than relabeling it as 429.
|
|
1249
|
-
logCtx.terminalHttpStatus = candidates.some(candidate => typeof candidate?.message === "string"
|
|
1250
|
-
&& Number(candidate.message.trim()) === 402) ? 402 : 429;
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
if (type !== "response.failed" || !responseError || typeof responseError !== "object") return;
|
|
1254
|
-
const responseCode = responseError.code === null || typeof responseError.code === "string"
|
|
1255
|
-
? responseError.code
|
|
1256
|
-
: undefined;
|
|
1257
|
-
logCtx.terminalHttpStatus = httpStatusFromTerminalError({
|
|
1258
|
-
type: typeof responseError.type === "string" ? responseError.type : undefined,
|
|
1259
|
-
code: responseCode,
|
|
1260
|
-
message: typeof responseError.message === "string" ? responseError.message : undefined,
|
|
1261
|
-
});
|
|
1262
|
-
}
|
|
1263
|
-
|
|
1264
1161
|
/** Map a terminal Responses error object to the HTTP status we record in /api/logs. */
|
|
1265
1162
|
export function httpStatusFromTerminalError(error: {
|
|
1266
1163
|
type?: string;
|
|
@@ -1467,12 +1364,12 @@ export function addFinalRequestLog(
|
|
|
1467
1364
|
const effectiveStatus = status >= 500 && logCtx.upstreamError && isClientClosedMessage(logCtx.upstreamError)
|
|
1468
1365
|
? 499
|
|
1469
1366
|
: status;
|
|
1470
|
-
//
|
|
1471
|
-
// status-plus-upstream-message classification can reconstruct.
|
|
1367
|
+
// Locally assigned codes name proxy refusals that status and upstream text cannot reconstruct.
|
|
1472
1368
|
const errorCode = logCtx.errorCode ?? requestLogErrorCode(
|
|
1473
1369
|
effectiveStatus,
|
|
1474
1370
|
logCtx.upstreamError,
|
|
1475
1371
|
logCtx.terminalErrorCode,
|
|
1372
|
+
logCtx.causeHint === "transport-ambiguous" && logCtx.terminalSource === "synthetic",
|
|
1476
1373
|
);
|
|
1477
1374
|
// A response.failed whose classified status is 499 is still a client cancel, not an upstream
|
|
1478
1375
|
// terminal failure — keep /api/logs closeReason aligned with that.
|
|
@@ -1502,6 +1399,7 @@ export function addFinalRequestLog(
|
|
|
1502
1399
|
...(logCtx.transportPhase ? { transportPhase: logCtx.transportPhase } : {}),
|
|
1503
1400
|
...(logCtx.terminalSource ? { terminalSource: logCtx.terminalSource } : {}),
|
|
1504
1401
|
outputObserved: logCtx.firstOutputMs !== undefined,
|
|
1402
|
+
...(logCtx.causeHint ? { causeHint: logCtx.causeHint } : {}),
|
|
1505
1403
|
locallyAnswered: logCtx.localTerminalReason !== undefined,
|
|
1506
1404
|
...(logCtx.activeAttempt ? { attempt: logCtx.activeAttempt } : {}),
|
|
1507
1405
|
});
|
|
@@ -1620,8 +1518,7 @@ export function addFinalRequestLog(
|
|
|
1620
1518
|
...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
|
|
1621
1519
|
...(closeReason ? { closeReason } : {}),
|
|
1622
1520
|
...(logCtx.upstreamError ? { upstreamError: logCtx.upstreamError } : {}),
|
|
1623
|
-
...(logCtx
|
|
1624
|
-
...(logCtx.upstreamRequestId ? { upstreamRequestId: logCtx.upstreamRequestId } : {}),
|
|
1521
|
+
...upstreamDiagnosticLogFields(logCtx),
|
|
1625
1522
|
usageStatus,
|
|
1626
1523
|
...(loggedUsage ? { usage: loggedUsage } : {}),
|
|
1627
1524
|
...(totalTokens !== undefined ? { totalTokens } : {}),
|
|
@@ -83,6 +83,7 @@ export function createAdapterContinuations(
|
|
|
83
83
|
| "oauthDispatch"
|
|
84
84
|
| "invalidateSameTargetRequest"
|
|
85
85
|
| "resolveSelectionAdapter"
|
|
86
|
+
| "resolveCopilotSelection"
|
|
86
87
|
| "anthropicInstance"
|
|
87
88
|
| "currentAnthropicRouteDecision"
|
|
88
89
|
| "anthropicRouteDecision"
|
|
@@ -156,7 +157,8 @@ export function createAdapterContinuations(
|
|
|
156
157
|
/**
|
|
157
158
|
* One bounded internal re-ask for Anthropic end_turn-without-tool-call turns. Replays the
|
|
158
159
|
* continuation on a 429 with the same-key retry budget (hoisted per request), then falls
|
|
159
|
-
* back to key/account failover
|
|
160
|
+
* back to key/account failover, rebuilding Copilot model, wire, and credential negotiation.
|
|
161
|
+
* A failure becomes an in-stream adapter error so the client
|
|
160
162
|
* never sees a second hidden HTTP response or an unbounded retry loop.
|
|
161
163
|
*/
|
|
162
164
|
const fetchTerminalGuardContinuation = async function* (
|
|
@@ -377,6 +379,12 @@ export function createAdapterContinuations(
|
|
|
377
379
|
if (rotated) {
|
|
378
380
|
try { void response.body?.cancel().catch(() => {}); } catch { /* already closed */ }
|
|
379
381
|
route.provider = rotated;
|
|
382
|
+
try { await transportState.resolveCopilotSelection(nextParsed); }
|
|
383
|
+
catch (error) {
|
|
384
|
+
yield { type: "error", message: `Provider continuation failed: ${redactSecretString(error instanceof Error ? error.message : String(error))}`,
|
|
385
|
+
...(options.abortSignal?.aborted || upstream.signal.aborted ? { status: 499 } : {}) };
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
380
388
|
invalidateSameTargetRequest();
|
|
381
389
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
382
390
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spendLedgerStorageErrorResponse } from "./spend-storage-error";
|
|
1
2
|
import { classifyAnthropic429 } from "../../oauth/anthropic-rate-limit-policy";
|
|
2
3
|
import { rotateAnthropicAccountOnResponseForInstance } from "../../oauth/anthropic-account-refusal";
|
|
3
4
|
import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
@@ -134,12 +135,15 @@ export async function prepareAdapterExchange(
|
|
|
134
135
|
| "transportToken"
|
|
135
136
|
| "oauthDispatch"
|
|
136
137
|
| "imageTierBias"
|
|
138
|
+
| "oauth401ReplayAttempted"
|
|
137
139
|
| "isOAuth401ReplayProvider"
|
|
138
140
|
| "sentOAuthSnapshot"
|
|
139
141
|
| "refreshResolvedOAuthSelection"
|
|
140
142
|
| "replayOAuthCredentialSnapshot"
|
|
141
143
|
| "invalidateSameTargetRequest"
|
|
142
144
|
| "resolveSelectionAdapter"
|
|
145
|
+
| "resolveCopilotSelection"
|
|
146
|
+
| "copilotRefusalResponse"
|
|
143
147
|
| "anthropicInstance"
|
|
144
148
|
| "currentAnthropicRouteDecision"
|
|
145
149
|
| "anthropicRouteDecision"
|
|
@@ -156,6 +160,7 @@ export async function prepareAdapterExchange(
|
|
|
156
160
|
responseEffects: Pick<ResponsesEffects, "cancelResponseCompletion" | "notifyResponseComplete" | "refreshRequestToolAliases">,
|
|
157
161
|
sendBudgetState: Pick<
|
|
158
162
|
ResponsesSendBudget,
|
|
163
|
+
| "adapterSendBudget"
|
|
159
164
|
| "adapterDispatchBudget"
|
|
160
165
|
| "noteAdapterPhysicalSend"
|
|
161
166
|
| "noteAdapterRecoveryWithheld"
|
|
@@ -182,6 +187,8 @@ export async function prepareAdapterExchange(
|
|
|
182
187
|
refreshResolvedOAuthSelection,
|
|
183
188
|
invalidateSameTargetRequest,
|
|
184
189
|
resolveSelectionAdapter,
|
|
190
|
+
resolveCopilotSelection,
|
|
191
|
+
copilotRefusalResponse,
|
|
185
192
|
anthropicInstance,
|
|
186
193
|
currentAnthropicRouteDecision,
|
|
187
194
|
anthropicSessionKey,
|
|
@@ -199,6 +206,7 @@ export async function prepareAdapterExchange(
|
|
|
199
206
|
} = requestState;
|
|
200
207
|
const { cancelResponseCompletion, notifyResponseComplete, refreshRequestToolAliases } = responseEffects;
|
|
201
208
|
const {
|
|
209
|
+
adapterSendBudget,
|
|
202
210
|
adapterDispatchBudget,
|
|
203
211
|
noteAdapterPhysicalSend,
|
|
204
212
|
noteAdapterRecoveryWithheld,
|
|
@@ -222,6 +230,17 @@ export async function prepareAdapterExchange(
|
|
|
222
230
|
|
|
223
231
|
const upstream = new AbortController();
|
|
224
232
|
const cleanupUpstreamAbort = linkAbortSignal(upstream, options.abortSignal);
|
|
233
|
+
/** Contain replacement negotiation before adapter admission and release its upstream abort link. */
|
|
234
|
+
const resolveRotatedCopilotSelection = async (): Promise<Response | undefined> => {
|
|
235
|
+
try { await resolveCopilotSelection(parsed); }
|
|
236
|
+
catch (error) {
|
|
237
|
+
cleanupUpstreamAbort();
|
|
238
|
+
upstream.abort();
|
|
239
|
+
if ((options.abortSignal ?? req.signal).aborted) return clientCancelledResponse();
|
|
240
|
+
return copilotRefusalResponse(error)
|
|
241
|
+
?? formatErrorResponse(502, "upstream_error", "GitHub Copilot Auto negotiation failed.");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
225
244
|
const connectMs = config.connectTimeoutMs ?? 200_000;
|
|
226
245
|
// Bridge stall budget (seconds of silence before upstream_stall_timeout); the retry backoff
|
|
227
246
|
// heartbeat interval is derived from it so the watchdog is always fed during deliberate waits.
|
|
@@ -338,10 +357,15 @@ export async function prepareAdapterExchange(
|
|
|
338
357
|
* is invisible to it and a missed bump would replay a request built with a stale key.
|
|
339
358
|
*/
|
|
340
359
|
|
|
360
|
+
const initialRecovery = route.providerName === "github-copilot"
|
|
361
|
+
? sendBudgetState.pendingHopPermit?.sendClass === "auth-recovery" ? "oauth-account-429"
|
|
362
|
+
: transportState.oauth401ReplayAttempted ? "oauth-401" : undefined
|
|
363
|
+
: undefined;
|
|
341
364
|
let upstreamResponse: Response;
|
|
365
|
+
let handoffPermit: ResponsesSendBudget["pendingHopPermit"];
|
|
342
366
|
try {
|
|
343
367
|
if (transportState.activeAdapter.fetchResponse) {
|
|
344
|
-
transportState.noteRoutedAttemptSend(inputTokenEstimate);
|
|
368
|
+
transportState.noteRoutedAttemptSend(inputTokenEstimate, initialRecovery);
|
|
345
369
|
const producer = adapterDispatchBudget?.beginSpendProducer?.();
|
|
346
370
|
try {
|
|
347
371
|
upstreamResponse = await withProviderRequestSlot(route.providerName, route.provider, route.modelId, upstream.signal, pacingSlot =>
|
|
@@ -374,7 +398,22 @@ export async function prepareAdapterExchange(
|
|
|
374
398
|
// reset-only semantics so combo failover hops on the first 5xx.
|
|
375
399
|
const transientPolicy = transientRetryPolicyFor(route.provider);
|
|
376
400
|
const resetPolicy = resetReplayPolicyFor(route.provider);
|
|
377
|
-
|
|
401
|
+
// Rebook a native Copilot hop against the negotiated destination before dispatch.
|
|
402
|
+
// Raw externally counted permits settle through the retry helper's send reporter.
|
|
403
|
+
if (route.providerName === "github-copilot" && adapterSendBudget && sendBudgetState.pendingHopPermit
|
|
404
|
+
&& sendBudgetState.pendingHopPermit.sendClass === "auth-recovery") {
|
|
405
|
+
const original = sendBudgetState.pendingHopPermit;
|
|
406
|
+
sendBudgetState.pendingHopPermit = undefined;
|
|
407
|
+
original.release();
|
|
408
|
+
const decision = adapterSendBudget.reserveDispatch({
|
|
409
|
+
sendClass: original.sendClass,
|
|
410
|
+
targetKey: `${route.providerName}|${builtInitialRequest.url}|${route.modelId}`,
|
|
411
|
+
countedExternally: true,
|
|
412
|
+
});
|
|
413
|
+
if (!decision.allowed) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
|
|
414
|
+
handoffPermit = decision.permit;
|
|
415
|
+
}
|
|
416
|
+
const compactPrepaid = handoffPermit ?? (options.compactionRecoveryAttempted ? sendBudgetState.pendingHopPermit : undefined);
|
|
378
417
|
if (compactPrepaid) sendBudgetState.pendingHopPermit = undefined;
|
|
379
418
|
let compactPrepaidUsed = false;
|
|
380
419
|
// A combo-owned or compaction-prepaid send is settled by the physical-dispatch receipt
|
|
@@ -407,7 +446,7 @@ export async function prepareAdapterExchange(
|
|
|
407
446
|
if (!compactPrepaid.use()) throw new SendBudgetExhaustedError(safeHostLabel(builtInitialRequest.url));
|
|
408
447
|
compactPrepaidUsed = true;
|
|
409
448
|
}
|
|
410
|
-
transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery);
|
|
449
|
+
transportState.noteRoutedAttemptSend(inputTokenEstimate, recovery ?? initialRecovery);
|
|
411
450
|
return fetchWithHeaderTimeout(builtInitialRequest.url, applyUpstreamRecoveryInit({
|
|
412
451
|
method: builtInitialRequest.method,
|
|
413
452
|
headers: builtInitialRequest.headers,
|
|
@@ -456,6 +495,11 @@ export async function prepareAdapterExchange(
|
|
|
456
495
|
cleanupUpstreamAbort();
|
|
457
496
|
upstream.abort();
|
|
458
497
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
498
|
+
const storageRefusal = spendLedgerStorageErrorResponse(err, logCtx);
|
|
499
|
+
if (storageRefusal) {
|
|
500
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
501
|
+
return storageRefusal;
|
|
502
|
+
}
|
|
459
503
|
const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
|
|
460
504
|
const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
461
505
|
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
@@ -486,6 +530,7 @@ export async function prepareAdapterExchange(
|
|
|
486
530
|
const msg = describeUpstreamConnectFailure(err, connectMs);
|
|
487
531
|
return formatErrorResponse(502, "upstream_error", msg);
|
|
488
532
|
} finally {
|
|
533
|
+
handoffPermit?.release();
|
|
489
534
|
builtInitialRequest.releaseBodyObservation?.();
|
|
490
535
|
}
|
|
491
536
|
|
|
@@ -509,7 +554,7 @@ export async function prepareAdapterExchange(
|
|
|
509
554
|
// Console Go answers a transient 400 "Invalid upload request." for bodies it accepts
|
|
510
555
|
// moments later; at most one byte-identical replay is allowed per request.
|
|
511
556
|
const consoleGoUploadRetryGuard: { attempted: boolean } = { attempted: false };
|
|
512
|
-
let oauth401ReplayAttempted =
|
|
557
|
+
let oauth401ReplayAttempted = transportState.oauth401ReplayAttempted;
|
|
513
558
|
let antigravityAuthRotationAttempted = false;
|
|
514
559
|
// At most one reasoning-effort downgrade per request. This sits outside the recovery loop
|
|
515
560
|
// below for the same reason the two guards above do: a guard declared inside it is reset by
|
|
@@ -692,6 +737,13 @@ export async function prepareAdapterExchange(
|
|
|
692
737
|
retryRequest.releaseBodyObservation?.();
|
|
693
738
|
}
|
|
694
739
|
} catch (err) {
|
|
740
|
+
const storageRefusal = !options.abortSignal?.aborted && spendLedgerStorageErrorResponse(err, logCtx);
|
|
741
|
+
if (storageRefusal) {
|
|
742
|
+
cleanupUpstreamAbort();
|
|
743
|
+
upstream.abort();
|
|
744
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
745
|
+
return { failed: storageRefusal };
|
|
746
|
+
}
|
|
695
747
|
const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
696
748
|
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
697
749
|
});
|
|
@@ -793,7 +845,7 @@ export async function prepareAdapterExchange(
|
|
|
793
845
|
&& !oauth401ReplayAttempted
|
|
794
846
|
&& !sendBudgetExhausted()
|
|
795
847
|
) {
|
|
796
|
-
oauth401ReplayAttempted = true;
|
|
848
|
+
oauth401ReplayAttempted = transportState.oauth401ReplayAttempted = true;
|
|
797
849
|
let refreshed: OAuthAccessSnapshot;
|
|
798
850
|
try {
|
|
799
851
|
refreshed = await refreshResolvedOAuthSelection(transportState.sentOAuthSnapshot);
|
|
@@ -864,7 +916,7 @@ export async function prepareAdapterExchange(
|
|
|
864
916
|
if (route.providerName === "kiro") {
|
|
865
917
|
parsed._kiroAuthContext = { ...(refreshed.kiro ?? {}) };
|
|
866
918
|
}
|
|
867
|
-
|
|
919
|
+
let refreshedProvider = resolveProviderTransport(
|
|
868
920
|
route.providerName,
|
|
869
921
|
{
|
|
870
922
|
...route.provider,
|
|
@@ -877,6 +929,9 @@ export async function prepareAdapterExchange(
|
|
|
877
929
|
: undefined,
|
|
878
930
|
);
|
|
879
931
|
route.provider = refreshedProvider;
|
|
932
|
+
const copilotRefusal = await resolveRotatedCopilotSelection();
|
|
933
|
+
if (copilotRefusal) return copilotRefusal;
|
|
934
|
+
refreshedProvider = route.provider;
|
|
880
935
|
invalidateSameTargetRequest();
|
|
881
936
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
882
937
|
resolveWireProtocolOverride(route.providerName, route.modelId, refreshedProvider, inboundWire, route.staticPolicy),
|
|
@@ -929,6 +984,8 @@ export async function prepareAdapterExchange(
|
|
|
929
984
|
// until runtime cleanup (one per rotated key).
|
|
930
985
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
931
986
|
route.provider = rotated;
|
|
987
|
+
const selectionFailure = await resolveRotatedCopilotSelection();
|
|
988
|
+
if (selectionFailure) return selectionFailure;
|
|
932
989
|
invalidateSameTargetRequest();
|
|
933
990
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
934
991
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|
|
@@ -1075,6 +1132,8 @@ export async function prepareAdapterExchange(
|
|
|
1075
1132
|
// until runtime cleanup (one per rotated key under a rate-limit storm).
|
|
1076
1133
|
try { void upstreamResponse.body?.cancel().catch(() => {}); } catch { /* already consumed/closed */ }
|
|
1077
1134
|
route.provider = rotated;
|
|
1135
|
+
const selectionFailure = await resolveRotatedCopilotSelection();
|
|
1136
|
+
if (selectionFailure) return selectionFailure;
|
|
1078
1137
|
invalidateSameTargetRequest();
|
|
1079
1138
|
transportState.activeAdapter = resolveSelectionAdapter(
|
|
1080
1139
|
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire, route.staticPolicy),
|