@bitkyc08/opencodex 2.78.0 → 2.80.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-CSJvmjNs.css +1 -0
- package/gui/dist/assets/App-MbDVfUeC.js +54 -0
- package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-CzkxK82A.js} +1 -1
- package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
- package/gui/dist/assets/index-UJt6npXk.js +94 -0
- package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-DU3sD6Z-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin.ts +37 -2
- package/src/adapters/openai-chat/messages.ts +3 -1
- package/src/claude/desktop-picker.ts +11 -9
- package/src/claude/inbound-content-options.ts +29 -2
- package/src/claude/inbound.ts +9 -0
- package/src/claude/intercept/local-ca-files.ts +265 -0
- package/src/claude/intercept/local-ca-windows.ts +73 -0
- package/src/claude/intercept/local-ca.ts +12 -24
- package/src/claude/intercept/picker-ca-persistence.ts +106 -0
- package/src/claude/intercept/picker-ca-startup.ts +16 -0
- package/src/claude/intercept/picker-ca-store.ts +70 -0
- package/src/claude/intercept/picker-ca.ts +43 -9
- package/src/claude/intercept/picker-runtime.ts +14 -4
- package/src/claude/intercept/runtime.ts +32 -23
- package/src/cli/ensure-readiness.ts +48 -0
- package/src/cli/gui-pair-client.ts +15 -7
- package/src/cli/gui.ts +4 -1
- package/src/cli/hub.ts +1 -0
- package/src/cli/index.ts +10 -11
- package/src/cli/status.ts +2 -2
- package/src/cli/update-restart-child.ts +4 -1
- package/src/codex/account-credit-use.ts +8 -2
- package/src/codex/auth-api/account-pause-group.ts +113 -0
- package/src/codex/auth-api/routes.ts +18 -12
- package/src/codex/auth-context.ts +45 -3
- package/src/codex/autostart-health.ts +13 -3
- package/src/codex/journal.ts +36 -5
- package/src/codex/pool-credit-policy.ts +67 -0
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev.ts +3 -1
- package/src/generated/compatibility-version.json +135 -63
- package/src/lib/gui-pair-capability.ts +1 -1
- package/src/lib/gui-pair-intent.ts +170 -0
- package/src/lib/windows-owner-acl.ts +143 -0
- package/src/providers/openai-sidecar-credit.ts +26 -0
- package/src/providers/openai-sidecar.ts +35 -0
- package/src/server/claude-messages.ts +1 -0
- package/src/server/gui-pair-delivery.ts +23 -0
- package/src/server/index/serve-options.ts +5 -8
- package/src/server/index.ts +12 -14
- package/src/server/live-native-calls.ts +94 -0
- package/src/server/live.ts +28 -5
- package/src/server/local-management-read-client.ts +15 -11
- package/src/server/management/oauth-account-routes.ts +15 -0
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/shared.ts +9 -3
- package/src/server/messages-native-scope.ts +60 -0
- package/src/server/messages-native.ts +9 -0
- package/src/server/responses/adapter-dispatch.ts +31 -2
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +17 -2
- package/src/server/responses/core-auth.ts +18 -1
- package/src/server/responses/core-codex-account.ts +18 -8
- package/src/server/responses/core-combo.ts +5 -7
- package/src/server/responses/non-replayable-error.ts +48 -0
- package/src/server/responses/passthrough-delivery.ts +5 -1
- package/src/server/responses/passthrough-dispatch.ts +21 -19
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/sidecar-execution.ts +1 -0
- package/src/server/search.ts +4 -0
- package/src/server/startup-health-cache.ts +2 -6
- package/src/service/desktop-startup.ts +100 -3
- package/src/vision/describe.ts +17 -10
- package/src/vision/index.ts +1 -0
- package/src/web-search/executor.ts +17 -10
- package/src/web-search/format-result.ts +304 -20
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/loop.ts +7 -6
- package/src/web-search/passthrough-bridge.ts +1 -0
- package/src/web-search/run-turn-loop.ts +1 -1
- package/gui/dist/assets/App-BtfG5id-.js +0 -54
- package/gui/dist/assets/App-lCIslZ1L.css +0 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
package/src/server/live.ts
CHANGED
|
@@ -44,9 +44,14 @@ import { cancelBodyOnAbort, signalWithTimeout } from "../lib/abort";
|
|
|
44
44
|
import { sidecarEnter } from "../lib/sidecar-tracker";
|
|
45
45
|
import { hardenSecretPath } from "../lib/windows-secret-acl";
|
|
46
46
|
import type { OcxConfig } from "../types";
|
|
47
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
resolveCallerOwnedOpenAiSidecar,
|
|
49
|
+
resolveFirstUsableOpenAiSidecar,
|
|
50
|
+
selectOpenAiImagesProvider,
|
|
51
|
+
} from "../providers/openai-sidecar";
|
|
48
52
|
import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential, type DataPlaneAdmission } from "./auth-cors";
|
|
49
53
|
import { admissionScopeDenial } from "./admission-model-scope";
|
|
54
|
+
import { nativeLiveCalls } from "./live-native-calls";
|
|
50
55
|
import type { RequestLogContext } from "./request-log";
|
|
51
56
|
import { codexLogAccountId } from "./responses";
|
|
52
57
|
import type { AdmissionLease } from "../lib/admission";
|
|
@@ -613,6 +618,12 @@ async function readRequestBodyCapped(req: Request, maxBytes: number): Promise<Ar
|
|
|
613
618
|
export interface LiveScopeDestination {
|
|
614
619
|
admission?: DataPlaneAdmission;
|
|
615
620
|
model: string | undefined;
|
|
621
|
+
/**
|
|
622
|
+
* The call a sideband join attaches to. When this process did not create it (no entry in
|
|
623
|
+
* `nativeLiveCalls`), the client created it with its own ChatGPT login — a V3 `existingCall` —
|
|
624
|
+
* and the join is authenticated as that caller instead of a Pool-selected account.
|
|
625
|
+
*/
|
|
626
|
+
callerOwnedCallId?: string;
|
|
616
627
|
}
|
|
617
628
|
|
|
618
629
|
/**
|
|
@@ -691,9 +702,14 @@ export async function resolveLiveRelay(
|
|
|
691
702
|
let forwardAuthError: Response | undefined;
|
|
692
703
|
if (candidates.forwardCandidates.length > 0) {
|
|
693
704
|
try {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
705
|
+
const joinsForeignCall = destination?.callerOwnedCallId !== undefined
|
|
706
|
+
&& !nativeLiveCalls.has(destination.callerOwnedCallId);
|
|
707
|
+
forward = (joinsForeignCall
|
|
708
|
+
? resolveCallerOwnedOpenAiSidecar(candidates.forwardCandidates, req.headers, config)
|
|
709
|
+
: undefined)
|
|
710
|
+
?? await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config, {
|
|
711
|
+
beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
|
|
712
|
+
});
|
|
697
713
|
if (forward) {
|
|
698
714
|
logCtx.provider = formatCodexProviderForLog(
|
|
699
715
|
forward.providerName,
|
|
@@ -756,7 +772,8 @@ export async function resolveLiveRelay(
|
|
|
756
772
|
providerBaseUrl: provider.baseUrl,
|
|
757
773
|
usesBackendShape: isChatGptBackendBaseUrl(provider.baseUrl),
|
|
758
774
|
keyed: false,
|
|
759
|
-
|
|
775
|
+
// A caller-owned or Direct credential selected no Pool account, so there is nothing to record.
|
|
776
|
+
...(forward.recordOutcome ? { recordOutcome: (status: number | "timeout" | "connect_error") => forward.recordOutcome?.(status) } : {}),
|
|
760
777
|
};
|
|
761
778
|
}
|
|
762
779
|
if (forwardAuthError) return forwardAuthError;
|
|
@@ -864,6 +881,11 @@ export async function handleLive(
|
|
|
864
881
|
detachBodyGuard();
|
|
865
882
|
}
|
|
866
883
|
if (payload instanceof Response) return payload;
|
|
884
|
+
// Remember the calls this proxy created, so their joins keep the Pool account that created
|
|
885
|
+
// them while a join for any other call is authenticated as its caller (`resolveLiveRelay`).
|
|
886
|
+
if (upstreamResponse.status >= 200 && upstreamResponse.status < 300) {
|
|
887
|
+
nativeLiveCalls.record(upstreamResponse.headers.get("location"));
|
|
888
|
+
}
|
|
867
889
|
const relayHeaders: Record<string, string> = {};
|
|
868
890
|
for (const name of LIVE_RELAY_HEADERS) {
|
|
869
891
|
const value = upstreamResponse.headers.get(name);
|
|
@@ -902,6 +924,7 @@ export async function resolveLiveSidebandUpgrade(
|
|
|
902
924
|
const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease, {
|
|
903
925
|
admission,
|
|
904
926
|
model: liveSidebandModel(target),
|
|
927
|
+
...("callId" in target ? { callerOwnedCallId: target.callId } : {}),
|
|
905
928
|
});
|
|
906
929
|
if (relay instanceof Response) return relay;
|
|
907
930
|
return {
|
|
@@ -25,6 +25,8 @@ export type LocalManagementReadResult =
|
|
|
25
25
|
|
|
26
26
|
export interface LocalManagementReadDeps {
|
|
27
27
|
fetchImpl?: typeof fetch;
|
|
28
|
+
/** Default transport seam; receives the caller deadline. */
|
|
29
|
+
directFetch?: typeof directLocalHttpFetch;
|
|
28
30
|
readRuntime?: (pid: number) => RuntimePortState | null;
|
|
29
31
|
createNonce?: () => string;
|
|
30
32
|
now?: () => number;
|
|
@@ -84,18 +86,20 @@ export async function fetchBoundLocalManagementRead(
|
|
|
84
86
|
if (!capability) return { kind: "unavailable", reason: "capability-unavailable" };
|
|
85
87
|
|
|
86
88
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
signal: AbortSignal.timeout(deps.timeoutMs ?? 4_000),
|
|
89
|
+
const timeoutMs = deps.timeoutMs ?? 4_000;
|
|
90
|
+
const url = `http://${probeHostname(target.hostname)}:${target.port}${path}`;
|
|
91
|
+
const init: RequestInit = {
|
|
92
|
+
headers: {
|
|
93
|
+
[LOCAL_MANAGEMENT_EXPECTED_PID_HEADER]: String(target.pid),
|
|
94
|
+
[LOCAL_MANAGEMENT_NONCE_HEADER]: nonce,
|
|
95
|
+
[LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER]: String(expiresAt),
|
|
96
|
+
[LOCAL_MANAGEMENT_CAPABILITY_HEADER]: capability,
|
|
97
97
|
},
|
|
98
|
-
|
|
98
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
99
|
+
};
|
|
100
|
+
const response = deps.fetchImpl
|
|
101
|
+
? await deps.fetchImpl(url, init)
|
|
102
|
+
: await (deps.directFetch ?? directLocalHttpFetch)(url, init, { timeoutMs });
|
|
99
103
|
if (deps.requireResponseProof && !verifyLocalAttestationProof(
|
|
100
104
|
runtime.attestationSecret, nonce, target.pid, target.port, response.headers.get(LOCAL_ATTESTATION_PROOF_HEADER),
|
|
101
105
|
)) {
|
|
@@ -1164,6 +1164,21 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
1164
1164
|
}, 200, req, config);
|
|
1165
1165
|
}
|
|
1166
1166
|
|
|
1167
|
+
if (url.pathname === "/api/keys/reveal" && req.method === "POST") {
|
|
1168
|
+
if (principal !== "gui-session") {
|
|
1169
|
+
return jsonResponse({ error: "dashboard session required" }, 403, req, config);
|
|
1170
|
+
}
|
|
1171
|
+
const body = await readJsonBody(req);
|
|
1172
|
+
if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
|
|
1173
|
+
return jsonResponse({ error: "invalid body" }, 400, req, config);
|
|
1174
|
+
}
|
|
1175
|
+
const entry = config.apiKeys?.find(key => key.id === body.id);
|
|
1176
|
+
if (!entry) return jsonResponse({ error: "key not found" }, 404, req, config);
|
|
1177
|
+
const response = jsonResponse({ key: entry.key }, 200, req, config);
|
|
1178
|
+
response.headers.set("Cache-Control", "no-store");
|
|
1179
|
+
return response;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1167
1182
|
if (url.pathname === "/api/keys/rotate" && req.method === "POST") {
|
|
1168
1183
|
const body = await readJsonBody(req);
|
|
1169
1184
|
if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
|
|
@@ -324,6 +324,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
324
324
|
{ method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
325
325
|
{ method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
|
|
326
326
|
{ method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
327
|
+
{ method: "POST", path: "/api/keys/reveal", module: "server/management/oauth-account-routes", mutates: false, exempt: { reason: "session-only", why: "Revealing a data key requires the gui-session principal; the admin token is refused." } },
|
|
327
328
|
{ method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
|
|
328
329
|
{ method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true },
|
|
329
330
|
{ method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
|
|
@@ -89,6 +89,11 @@ export type TokPerSecondResult =
|
|
|
89
89
|
| { kind: "value"; value: number; estimated: boolean }
|
|
90
90
|
| { kind: "unavailable"; reason: MetricUnavailableReason };
|
|
91
91
|
|
|
92
|
+
export type DecodeTimingBasis = "generation-window" | "legacy-post-visible-output";
|
|
93
|
+
export type DecodeTokPerSecondResult =
|
|
94
|
+
| (Extract<TokPerSecondResult, { kind: "value" }> & { timingBasis: DecodeTimingBasis })
|
|
95
|
+
| Extract<TokPerSecondResult, { kind: "unavailable" }>;
|
|
96
|
+
|
|
92
97
|
export type CostEstimateReason =
|
|
93
98
|
| "usage_estimated"
|
|
94
99
|
| "cache_detail_missing"
|
|
@@ -149,7 +154,7 @@ export const MIN_DECODE_WINDOW_MS = 1_000;
|
|
|
149
154
|
*/
|
|
150
155
|
export function decodeTokPerSecondResult(
|
|
151
156
|
entry: Pick<MetricSource, "durationMs" | "firstOutputMs" | "genStartMs" | "lastOutputMs" | "usageStatus" | "usage">,
|
|
152
|
-
):
|
|
157
|
+
): DecodeTokPerSecondResult {
|
|
153
158
|
if (!entry.usage) return { kind: "unavailable", reason: "usage_missing" };
|
|
154
159
|
if (entry.usageStatus === "unsupported") return { kind: "unavailable", reason: "usage_unsupported" };
|
|
155
160
|
if (entry.usage.outputTokens <= 0) return { kind: "unavailable", reason: "output_missing" };
|
|
@@ -159,7 +164,8 @@ export function decodeTokPerSecondResult(
|
|
|
159
164
|
if (generationMs <= 0) return { kind: "unavailable", reason: "invalid_duration" };
|
|
160
165
|
if (generationMs < MIN_DECODE_WINDOW_MS) return { kind: "unavailable", reason: "decode_window_too_short" };
|
|
161
166
|
const value = tokensPerSecond(entry.usage.outputTokens, generationMs);
|
|
162
|
-
return value === null ? { kind: "unavailable", reason: "invalid_duration" }
|
|
167
|
+
return value === null ? { kind: "unavailable", reason: "invalid_duration" }
|
|
168
|
+
: { kind: "value", value, estimated: true, timingBasis: "generation-window" };
|
|
163
169
|
}
|
|
164
170
|
// A row that predates TTFT capture, or a non-streaming turn that never recorded one, has no
|
|
165
171
|
// window to measure. That is a different fact from a bad duration, so it gets its own reason.
|
|
@@ -173,7 +179,7 @@ export function decodeTokPerSecondResult(
|
|
|
173
179
|
if (windowMs < MIN_DECODE_WINDOW_MS) return { kind: "unavailable", reason: "decode_window_too_short" };
|
|
174
180
|
const value = tokensPerSecond(entry.usage.outputTokens, windowMs);
|
|
175
181
|
if (value === null) return { kind: "unavailable", reason: "invalid_duration" };
|
|
176
|
-
return { kind: "value", value, estimated: true };
|
|
182
|
+
return { kind: "value", value, estimated: true, timingBasis: "legacy-post-visible-output" };
|
|
177
183
|
}
|
|
178
184
|
|
|
179
185
|
export function unavailableCostReason(entry: MetricSource): MetricUnavailableReason {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AdmissionModelDeniedError, routeAllowedByScope, UNNAMED_DESTINATION_MODEL, type AdmissionModelScope,
|
|
3
|
+
} from "./admission-model-scope";
|
|
4
|
+
|
|
5
|
+
type Rec = Record<string, unknown>;
|
|
6
|
+
const isRec = (value: unknown): value is Rec => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7
|
+
const isAdvisor = (tool: Rec): boolean => typeof tool.type === "string" && /^advisor_\d{8}$/.test(tool.type);
|
|
8
|
+
|
|
9
|
+
/** Only executable declarations are policy inputs; schemas, arguments and results stay opaque. */
|
|
10
|
+
export function nativeMessagesToolScopeDenial(
|
|
11
|
+
scope: AdmissionModelScope | undefined,
|
|
12
|
+
providerName: string,
|
|
13
|
+
requestedModel: string,
|
|
14
|
+
body: Readonly<Rec>,
|
|
15
|
+
): AdmissionModelDeniedError | undefined {
|
|
16
|
+
if (!scope || scope.models.length === 0) return undefined;
|
|
17
|
+
const definitions = new Map<string, Rec[]>();
|
|
18
|
+
const withdrawn = new Set<string>();
|
|
19
|
+
const anonymous: Rec[] = [];
|
|
20
|
+
if (Array.isArray(body.tools)) for (const tool of body.tools) {
|
|
21
|
+
if (!isRec(tool)) continue;
|
|
22
|
+
if (typeof tool.name !== "string") { anonymous.push(tool); continue; }
|
|
23
|
+
const declarations = definitions.get(tool.name) ?? [];
|
|
24
|
+
declarations.push(tool);
|
|
25
|
+
definitions.set(tool.name, declarations);
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(body.messages)) for (const message of body.messages) {
|
|
28
|
+
if (!isRec(message) || message.role !== "system" || !Array.isArray(message.content)) continue;
|
|
29
|
+
// Upstream rejects tool changes in turn-scoped messages; do not depend on that. An addition
|
|
30
|
+
// always counts, and only a permanent removal withdraws a declaration (fail closed).
|
|
31
|
+
const permanent = message.clear_at === undefined || message.clear_at === "never";
|
|
32
|
+
for (const block of message.content) {
|
|
33
|
+
if (!isRec(block) || !isRec(block.tool)) continue;
|
|
34
|
+
const tool = block.tool;
|
|
35
|
+
if (block.type === "tool_addition" && tool.type === "tool_definition" && isRec(tool.definition)) {
|
|
36
|
+
const definition = tool.definition;
|
|
37
|
+
if (typeof definition.name !== "string") { anonymous.push(definition); continue; }
|
|
38
|
+
// A temporary replacement keeps the declaration it shadows in scope as well.
|
|
39
|
+
// These arrays belong to this check, not to the caller's declarations.
|
|
40
|
+
const declarations = permanent ? [] : definitions.get(definition.name) ?? [];
|
|
41
|
+
declarations.push(definition);
|
|
42
|
+
definitions.set(definition.name, declarations);
|
|
43
|
+
withdrawn.delete(definition.name);
|
|
44
|
+
} else if (tool.type === "tool_reference" && typeof tool.name === "string") {
|
|
45
|
+
if (block.type === "tool_removal" && permanent) withdrawn.add(tool.name);
|
|
46
|
+
if (block.type === "tool_addition") withdrawn.delete(tool.name);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Deferred definitions can still be loaded by tool search in the current turn.
|
|
51
|
+
const active = [...anonymous, ...[...definitions].flatMap(([name, tools]) => withdrawn.has(name) ? [] : tools)];
|
|
52
|
+
for (const tool of active) {
|
|
53
|
+
if (!isAdvisor(tool)) continue;
|
|
54
|
+
const modelId = typeof tool.model === "string" && tool.model.trim() ? tool.model : undefined;
|
|
55
|
+
if (modelId === undefined || !routeAllowedByScope(scope, { providerName, modelId })) {
|
|
56
|
+
return new AdmissionModelDeniedError(requestedModel, { providerName, modelId: modelId ?? UNNAMED_DESTINATION_MODEL });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -110,6 +110,8 @@ import { linkAbortSignal } from "./responses/core-lifetime";
|
|
|
110
110
|
import { attachRequestSpendTracker } from "./responses/request-spend";
|
|
111
111
|
import { workflowRefusalResponse } from "./workflow-refusal";
|
|
112
112
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
113
|
+
import { admissionModelDeniedResponse, type AdmissionModelScope } from "./admission-model-scope";
|
|
114
|
+
import { nativeMessagesToolScopeDenial } from "./messages-native-scope";
|
|
113
115
|
|
|
114
116
|
export {
|
|
115
117
|
isNativeMessagesRouteEligible,
|
|
@@ -159,6 +161,7 @@ export interface HandleNativeMessagesOptions {
|
|
|
159
161
|
callerAnthropicBeta?: string | null;
|
|
160
162
|
clientIdentity?: AnthropicClientIdentity;
|
|
161
163
|
sessionKey?: string | null;
|
|
164
|
+
modelScope?: AdmissionModelScope;
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
|
|
@@ -350,6 +353,12 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
350
353
|
return null;
|
|
351
354
|
};
|
|
352
355
|
|
|
356
|
+
const toolScopeDenial = nativeMessagesToolScopeDenial(options.modelScope, route.providerName, requestedModel, body);
|
|
357
|
+
if (toolScopeDenial) {
|
|
358
|
+
finishLog(403, toolScopeDenial.message);
|
|
359
|
+
return admissionModelDeniedResponse(toolScopeDenial);
|
|
360
|
+
}
|
|
361
|
+
|
|
353
362
|
try {
|
|
354
363
|
await prepareNativeBody(body, req.signal);
|
|
355
364
|
} catch (error) {
|
|
@@ -4,6 +4,7 @@ import { authorizeResendForRecovery } from "../../lib/request-resend-gate";
|
|
|
4
4
|
import { ambiguousResendAllowanceFor, selfContainedResponsesBody } from "./reset-replay";
|
|
5
5
|
import { transientSendCapFor } from "./request-send-budget";
|
|
6
6
|
import { isNonReplayableResponse } from "../../lib/upstream-retry";
|
|
7
|
+
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
7
8
|
import { isLocalUpstream } from "../../lib/local-upstream";
|
|
8
9
|
import type { ResponsesRequestContext, ResponsesAdmissionState } from "./core-options";
|
|
9
10
|
import type { PreparedResponsesRequest } from "./request-prepare";
|
|
@@ -100,6 +101,9 @@ import {
|
|
|
100
101
|
} from "../../lib/errors";
|
|
101
102
|
import { resolveClientRetryAfter } from "../../lib/retry-after";
|
|
102
103
|
import { cancelBodyOnAbort } from "../../lib/abort";
|
|
104
|
+
import { createCodexAuthDispatchGuard, releaseCodexAuthContextProbeLease, unwrapUpstreamRetryEvidenceError } from "../../codex/auth-context";
|
|
105
|
+
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
106
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
103
107
|
import { chargeWorkflowSends } from "../../lib/workflow-budget";
|
|
104
108
|
import { isAntigravityValidationRefusal } from "./antigravity-validation-refusal";
|
|
105
109
|
|
|
@@ -344,6 +348,8 @@ export async function prepareAdapterExchange(
|
|
|
344
348
|
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
345
349
|
providerName: route.providerName,
|
|
346
350
|
modelId: route.modelId,
|
|
351
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
352
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
347
353
|
}),
|
|
348
354
|
}));
|
|
349
355
|
} else {
|
|
@@ -384,6 +390,8 @@ export async function prepareAdapterExchange(
|
|
|
384
390
|
dispatchOverride: oauthDispatch(builtInitialRequest),
|
|
385
391
|
providerName: route.providerName,
|
|
386
392
|
modelId: route.modelId,
|
|
393
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
394
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
387
395
|
}));
|
|
388
396
|
},
|
|
389
397
|
{
|
|
@@ -406,6 +414,13 @@ export async function prepareAdapterExchange(
|
|
|
406
414
|
upstream.abort();
|
|
407
415
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
408
416
|
const refusal = err instanceof UpstreamRetryEvidenceError ? err.cause : err;
|
|
417
|
+
const codexRefusal = mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
418
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
419
|
+
});
|
|
420
|
+
if (codexRefusal) {
|
|
421
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
422
|
+
return codexRefusal;
|
|
423
|
+
}
|
|
409
424
|
// A pause committed during pacing is local admission policy, not a failed upstream.
|
|
410
425
|
if (refusal instanceof OAuthAccountPausedError) {
|
|
411
426
|
return formatErrorResponse(403, "permission_error", publicOAuthAuthenticationErrorMessage(refusal));
|
|
@@ -550,6 +565,8 @@ export async function prepareAdapterExchange(
|
|
|
550
565
|
dispatchOverride: oauthDispatch(retryRequest),
|
|
551
566
|
providerName: route.providerName,
|
|
552
567
|
modelId: route.modelId,
|
|
568
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
569
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
553
570
|
}),
|
|
554
571
|
});
|
|
555
572
|
});
|
|
@@ -601,6 +618,8 @@ export async function prepareAdapterExchange(
|
|
|
601
618
|
dispatchOverride: oauthDispatch(retryRequest),
|
|
602
619
|
providerName: route.providerName,
|
|
603
620
|
modelId: route.modelId,
|
|
621
|
+
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
622
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
604
623
|
}));
|
|
605
624
|
},
|
|
606
625
|
{
|
|
@@ -624,6 +643,15 @@ export async function prepareAdapterExchange(
|
|
|
624
643
|
retryRequest.releaseBodyObservation?.();
|
|
625
644
|
}
|
|
626
645
|
} catch (err) {
|
|
646
|
+
const codexRefusal = !options.abortSignal?.aborted && mapCodexAuthContextErrorToResponse(unwrapUpstreamRetryEvidenceError(err), {
|
|
647
|
+
now: Date.now(), accountSelector: route.codexAccountNamespace,
|
|
648
|
+
});
|
|
649
|
+
if (codexRefusal) {
|
|
650
|
+
cleanupUpstreamAbort();
|
|
651
|
+
upstream.abort();
|
|
652
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
653
|
+
return { failed: codexRefusal };
|
|
654
|
+
}
|
|
627
655
|
if (preserveFailureResponse && !replacementAdmitted && !options.abortSignal?.aborted)
|
|
628
656
|
return { failed: preserveFailureResponse };
|
|
629
657
|
cleanupUpstreamAbort();
|
|
@@ -704,8 +732,9 @@ export async function prepareAdapterExchange(
|
|
|
704
732
|
// Preserve the terminal verdict through adapter and combo error formatting.
|
|
705
733
|
// This also covers a reset reached by a 401/429/413 recovery refetch.
|
|
706
734
|
if (isNonReplayableResponse(upstreamResponse)) {
|
|
707
|
-
|
|
708
|
-
|
|
735
|
+
try {
|
|
736
|
+
return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
|
|
737
|
+
} finally { cleanupUpstreamAbort(); }
|
|
709
738
|
}
|
|
710
739
|
if (
|
|
711
740
|
upstreamResponse.status === 401
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Restore the caller label while retaining JEV's forced effort and later child transitions. */
|
|
2
|
+
export function comboRequestedEffortLabel(
|
|
3
|
+
originalRequestedEffort: string | undefined,
|
|
4
|
+
childRequestedEffort: string | undefined,
|
|
5
|
+
forcedEffort?: string | null,
|
|
6
|
+
): string | undefined {
|
|
7
|
+
if (originalRequestedEffort === undefined) return childRequestedEffort;
|
|
8
|
+
const transitionIndex = childRequestedEffort?.indexOf("->") ?? -1;
|
|
9
|
+
const laterTransitions = transitionIndex >= 0 ? childRequestedEffort!.slice(transitionIndex) : "";
|
|
10
|
+
const forcedTransition = forcedEffort != null && forcedEffort !== originalRequestedEffort
|
|
11
|
+
? `->${forcedEffort}`
|
|
12
|
+
: "";
|
|
13
|
+
return `${originalRequestedEffort}${forcedTransition}${laterTransitions}`;
|
|
14
|
+
}
|
|
@@ -53,7 +53,7 @@ import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/
|
|
|
53
53
|
import {
|
|
54
54
|
applyCodexAuthContextToProvider,
|
|
55
55
|
callerCodexWorkspaceAccountId,
|
|
56
|
-
|
|
56
|
+
createCodexAuthDispatchGuard,
|
|
57
57
|
unwrapUpstreamRetryEvidenceError,
|
|
58
58
|
CodexMainProfileDrainingError,
|
|
59
59
|
headersForCodexAuthContext,
|
|
@@ -182,6 +182,8 @@ import {
|
|
|
182
182
|
import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
|
|
183
183
|
import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
|
|
184
184
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
185
|
+
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
186
|
+
import { clientCancelledResponse } from "./core-errors";
|
|
185
187
|
|
|
186
188
|
import {
|
|
187
189
|
codexAccountGatedCanonicalWireModel,
|
|
@@ -435,6 +437,9 @@ async function refreshPoolCompactContext(args: {
|
|
|
435
437
|
generation: refreshed.generation,
|
|
436
438
|
poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
|
|
437
439
|
};
|
|
440
|
+
// See the core counterpart: the spread dropped the WeakMap-bound credit policy, so a hold
|
|
441
|
+
// or opt-out landing during the refresh await would go unchecked on the copy.
|
|
442
|
+
rebindPoolCreditPolicy(authCtx, refreshedAuthCtx);
|
|
438
443
|
const refreshedProvider = applyCodexAuthContextToProvider(
|
|
439
444
|
stripCodexRuntimeProviderFields(provider),
|
|
440
445
|
refreshedAuthCtx,
|
|
@@ -463,6 +468,16 @@ async function refreshPoolCompactContext(args: {
|
|
|
463
468
|
if (isTerminalCompactPoolRefreshFailure(error)) {
|
|
464
469
|
return { ok: false, quarantine: true, response: reauthResponse() };
|
|
465
470
|
}
|
|
471
|
+
if (req.signal.aborted) {
|
|
472
|
+
return { ok: false, quarantine: false, response: clientCancelledResponse() };
|
|
473
|
+
}
|
|
474
|
+
// See the core counterpart: a policy refusal raised after a successful refresh maps like
|
|
475
|
+
// the admission path — a reset-bound 429, never a 503 mislabeled as a refresh failure.
|
|
476
|
+
const policyResponse = mapCodexAuthContextErrorToResponse(error, {
|
|
477
|
+
now: Date.now(),
|
|
478
|
+
accountSelector: args.codexAccountNamespace,
|
|
479
|
+
});
|
|
480
|
+
if (policyResponse) return { ok: false, quarantine: false, response: policyResponse };
|
|
466
481
|
return {
|
|
467
482
|
ok: false,
|
|
468
483
|
quarantine: false,
|
|
@@ -1042,7 +1057,7 @@ export async function handleResponsesCompact(
|
|
|
1042
1057
|
providerName: route.providerName,
|
|
1043
1058
|
modelId: route.modelId,
|
|
1044
1059
|
beforeDispatch: isCanonicalOpenAiForwardProvider(sendProvider)
|
|
1045
|
-
?
|
|
1060
|
+
? createCodexAuthDispatchGuard(sendAuthCtx, config, selectedModelId, admission) : undefined,
|
|
1046
1061
|
}),
|
|
1047
1062
|
// Every credential-bearing forward send gets manual redirects, not only
|
|
1048
1063
|
// pool sends: direct mode carries the caller's credential too (#914).
|
|
@@ -41,6 +41,7 @@ import { markLocalRequestLogRefusal } from "../request-log";
|
|
|
41
41
|
import { CODEX_POOL_REFRESH_INCOMPLETE_LOG_REASON } from "../../codex/pool-refresh-backoff";
|
|
42
42
|
import { codexAuthContextLogLabel } from "../../codex/account-label";
|
|
43
43
|
import { forceRefreshMainAccountToken } from "../../codex/main-account";
|
|
44
|
+
import { rebindPoolCreditPolicy } from "../../codex/pool-credit-policy";
|
|
44
45
|
import { safeCallerSessionId } from "../caller-session-identity";
|
|
45
46
|
|
|
46
47
|
/** Normalize caller aliases only on native egress; preserve ingress and fallback identity. */
|
|
@@ -200,6 +201,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
200
201
|
credentialDomainWasRewritten = false,
|
|
201
202
|
retainAccountForUploadedFiles = false,
|
|
202
203
|
): Promise<ResponsesAuthResolution> {
|
|
204
|
+
let authCtx: CodexAuthContext | undefined;
|
|
203
205
|
try {
|
|
204
206
|
let authInputHeaders = codexRouteCredentialDomainHeaders(
|
|
205
207
|
req,
|
|
@@ -260,7 +262,6 @@ export async function resolveResponsesCodexAuth(
|
|
|
260
262
|
if (route.codexAccountMode === "direct" && !substituteMainCredential) {
|
|
261
263
|
validateForwardAdmissionCredential(authInputHeaders, config);
|
|
262
264
|
}
|
|
263
|
-
let authCtx: CodexAuthContext;
|
|
264
265
|
if (route.codexAccountMode) {
|
|
265
266
|
authCtx = await resolveCodexAuthContext(authInputHeaders, config, route.codexAccountMode, {
|
|
266
267
|
admission: options.admission,
|
|
@@ -342,6 +343,7 @@ export async function resolveResponsesCodexAuth(
|
|
|
342
343
|
substituteMainCredential,
|
|
343
344
|
};
|
|
344
345
|
} catch (err) {
|
|
346
|
+
releaseCodexAuthContextProbeLease(authCtx);
|
|
345
347
|
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
346
348
|
return { ok: false, response: clientCancelledResponse() };
|
|
347
349
|
}
|
|
@@ -480,6 +482,9 @@ export async function refreshPoolForwardAuth(args: {
|
|
|
480
482
|
generation: refreshed.generation,
|
|
481
483
|
poolQuotaWriter: capturePoolQuotaWriter(authCtx.accountId, refreshed),
|
|
482
484
|
};
|
|
485
|
+
// The spread dropped the WeakMap-bound credit policy: without the live binding a hold or
|
|
486
|
+
// opt-out landing during the refresh await would go unchecked on every no-override path.
|
|
487
|
+
rebindPoolCreditPolicy(authCtx, refreshedAuthCtx);
|
|
483
488
|
const provider = applyCodexAuthContextToProvider(
|
|
484
489
|
stripCodexRuntimeProviderFields(route.provider),
|
|
485
490
|
refreshedAuthCtx,
|
|
@@ -502,6 +507,18 @@ export async function refreshPoolForwardAuth(args: {
|
|
|
502
507
|
response: formatErrorResponse(401, "authentication_error", "Selected Codex account needs reauthentication"),
|
|
503
508
|
};
|
|
504
509
|
}
|
|
510
|
+
if (options.abortSignal?.aborted || req.signal.aborted) {
|
|
511
|
+
return { ok: false, quarantine: false, response: clientCancelledResponse() };
|
|
512
|
+
}
|
|
513
|
+
// The credential itself refreshed; a policy refusal raised afterwards (a credit hold or an
|
|
514
|
+
// opt-out that landed during the await) is not a refresh failure. Map it the same way the
|
|
515
|
+
// admission path does — a reset-bound 429, never a 503 telling the caller to sign in — and
|
|
516
|
+
// never quarantine a valid account for a policy refusal.
|
|
517
|
+
const policyResponse = mapCodexAuthContextErrorToResponse(error, {
|
|
518
|
+
accountSelector: route.codexAccountNamespace,
|
|
519
|
+
now: Date.now(),
|
|
520
|
+
});
|
|
521
|
+
if (policyResponse) return { ok: false, quarantine: false, response: policyResponse };
|
|
505
522
|
return {
|
|
506
523
|
ok: false,
|
|
507
524
|
quarantine: false,
|
|
@@ -54,7 +54,8 @@ import {
|
|
|
54
54
|
headersForCodexAuthContext,
|
|
55
55
|
applyCodexAuthContextToProvider,
|
|
56
56
|
stripCodexRuntimeProviderFields,
|
|
57
|
-
|
|
57
|
+
createCodexAuthDispatchGuard,
|
|
58
|
+
CodexPoolAccountCreditsOffError,
|
|
58
59
|
} from "../../codex/auth-context";
|
|
59
60
|
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "../../codex/catalog/native-models";
|
|
60
61
|
import { isRequestExecutionBudget } from "../../lib/request-execution-budget";
|
|
@@ -745,12 +746,21 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
745
746
|
// Only a combo reset-derived outcome is deferred. Retry-After, defaults, and
|
|
746
747
|
// ordinary requests must block the first account before the alternate send.
|
|
747
748
|
if (!deferFirstOutcome) recordFirstOutcome();
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
retryAuthCtx,
|
|
752
|
-
"pool"
|
|
753
|
-
)
|
|
749
|
+
let retryHeaders: Headers;
|
|
750
|
+
let retryProvider: ReturnType<typeof applyCodexAuthContextToProvider>;
|
|
751
|
+
try {
|
|
752
|
+
retryHeaders = headersForCodexAuthContext(callerAuthHeaders, retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission);
|
|
753
|
+
retryProvider = applyCodexAuthContextToProvider(stripCodexRuntimeProviderFields(route.provider), retryAuthCtx, "pool");
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (!(error instanceof CodexPoolAccountCreditsOffError)) throw error;
|
|
756
|
+
// Body/entitlement reads above can outlive credit consent. No alternate will send:
|
|
757
|
+
// release its reservation and probes, then let the owner map the policy refusal.
|
|
758
|
+
accountMovePermit?.release();
|
|
759
|
+
releaseCodexAuthContextProbeLease(firstAuthCtx);
|
|
760
|
+
releaseCodexAuthContextProbeLease(retryAuthCtx);
|
|
761
|
+
await firstResponse.body?.cancel().catch(() => undefined);
|
|
762
|
+
return { kind: "transport", error, authCtx: retryAuthCtx };
|
|
763
|
+
}
|
|
754
764
|
const retryAdapter = resolveAdapter(
|
|
755
765
|
resolveWireProtocolOverride(route.providerName, route.modelId, retryProvider, inboundWire, route.staticPolicy),
|
|
756
766
|
config.cacheRetention,
|
|
@@ -889,7 +899,7 @@ export async function retryCodexPoolOnAlternateAccount(
|
|
|
889
899
|
modelId: route.modelId,
|
|
890
900
|
onCodexWsQuota: codexWsQuotaObserver(retryAuthCtx, route.provider, route.modelId),
|
|
891
901
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
892
|
-
?
|
|
902
|
+
? createCodexAuthDispatchGuard(retryAuthCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
893
903
|
}),
|
|
894
904
|
// Credential-bearing forward send: never follow a redirect into a
|
|
895
905
|
// dead-host rejection after the credential was seen (#914).
|
|
@@ -106,6 +106,7 @@ import { mandatoryResponsesReasoningReplayUnavailable } from "./core-replay";
|
|
|
106
106
|
import { settleOperatorReplacement } from "../../lib/upstream-retry";
|
|
107
107
|
import { createComboProtocolLanes, dispatchNativeComboChild } from "./core-combo-native";
|
|
108
108
|
import { createJevModelInvoker } from "./jev-model-invoke";
|
|
109
|
+
import { comboRequestedEffortLabel } from "./combo-requested-effort";
|
|
109
110
|
import { clientWireOf } from "../inference/client-wire";
|
|
110
111
|
|
|
111
112
|
/**
|
|
@@ -656,13 +657,9 @@ export async function executeComboResponses(
|
|
|
656
657
|
&& isDeclaredReasoningEffort(originalRequestedEffortValue)
|
|
657
658
|
? originalRequestedEffortValue
|
|
658
659
|
: undefined;
|
|
659
|
-
const restoreOriginalRequestedEffort = (childLog: RequestLogContext): void => {
|
|
660
|
+
const restoreOriginalRequestedEffort = (childLog: RequestLogContext, forcedEffort?: string | null): void => {
|
|
660
661
|
if (originalRequestedEffort === undefined) return;
|
|
661
|
-
|
|
662
|
-
const transitionIndex = normalizedRequestedEffort?.indexOf("->") ?? -1;
|
|
663
|
-
childLog.requestedEffort = transitionIndex >= 0
|
|
664
|
-
? `${originalRequestedEffort}${normalizedRequestedEffort!.slice(transitionIndex)}`
|
|
665
|
-
: originalRequestedEffort;
|
|
662
|
+
childLog.requestedEffort = comboRequestedEffortLabel(originalRequestedEffort, childLog.requestedEffort, forcedEffort);
|
|
666
663
|
recordAttemptRequestedEffort(childLog);
|
|
667
664
|
};
|
|
668
665
|
|
|
@@ -907,7 +904,8 @@ export async function executeComboResponses(
|
|
|
907
904
|
onNativePassthroughCancel: callbackGate.onCancel,
|
|
908
905
|
onResponseComplete: callbackGate.onResponseComplete,
|
|
909
906
|
});
|
|
910
|
-
|
|
907
|
+
// Native Chat rebuilds its own body and does not apply the initial JEV effort.
|
|
908
|
+
restoreOriginalRequestedEffort(childLog, nativeChild ? undefined : initialJevDecision?.effort);
|
|
911
909
|
} catch (error) {
|
|
912
910
|
callbackGate.discard();
|
|
913
911
|
if (options.abortSignal?.aborted) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isNonReplayableResponse, isReplayRefusalResponse, markResponseNonReplayable } from "../../lib/upstream-retry";
|
|
2
|
+
import { clientCancelledResponse, readDisplaySafeErrorText } from "./core-errors";
|
|
3
|
+
|
|
4
|
+
const UPSTREAM_ERROR_TYPES = new Set([
|
|
5
|
+
"invalid_request_error", "authentication_error", "permission_error", "not_found_error",
|
|
6
|
+
"rate_limit_error", "request_too_large", "overloaded_error", "api_error", "server_error", "insufficient_quota",
|
|
7
|
+
]);
|
|
8
|
+
const UPSTREAM_ERROR_CODES = new Set([
|
|
9
|
+
"context_length_exceeded", "rate_limit_exceeded", "insufficient_quota", "invalid_api_key",
|
|
10
|
+
"model_not_found", "invalid_prompt", "content_policy_violation", "server_error",
|
|
11
|
+
"unsupported_parameter", "invalid_value", "string_above_max_length",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
/** Client projection never grants another send or turns a real upstream error into our refusal. */
|
|
15
|
+
export async function sanitizeNonReplayableUpstreamError(
|
|
16
|
+
response: Response,
|
|
17
|
+
signal: AbortSignal,
|
|
18
|
+
): Promise<Response> {
|
|
19
|
+
if (response.ok || !isNonReplayableResponse(response) || isReplayRefusalResponse(response)) return response;
|
|
20
|
+
const text = await readDisplaySafeErrorText(response, signal, "");
|
|
21
|
+
if (signal.aborted) {
|
|
22
|
+
const cancelled = clientCancelledResponse();
|
|
23
|
+
markResponseNonReplayable(cancelled);
|
|
24
|
+
return cancelled;
|
|
25
|
+
}
|
|
26
|
+
let type = "upstream_error";
|
|
27
|
+
let code: string | undefined;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(text);
|
|
30
|
+
const upstreamType = parsed?.error?.type;
|
|
31
|
+
const upstreamCode = parsed?.error?.code;
|
|
32
|
+
if (typeof upstreamType === "string" && UPSTREAM_ERROR_TYPES.has(upstreamType)) type = upstreamType;
|
|
33
|
+
if (typeof upstreamCode === "string" && UPSTREAM_ERROR_CODES.has(upstreamCode)) code = upstreamCode;
|
|
34
|
+
} catch {
|
|
35
|
+
// Incomplete or non-JSON bodies retain the generic error type.
|
|
36
|
+
}
|
|
37
|
+
// Upstream text is not forwarded here: no bounded redaction can be proven complete against a client's decoders.
|
|
38
|
+
const body = JSON.stringify({ error: { type, ...(code === undefined ? {} : { code }),
|
|
39
|
+
message: `Provider error ${response.status}: upstream diagnostic withheld for a non-replayable failure`,
|
|
40
|
+
} });
|
|
41
|
+
const headers = new Headers({ "content-type": "application/json" });
|
|
42
|
+
if (response.headers.get("x-should-retry") === "false") headers.set("x-should-retry", "false");
|
|
43
|
+
const safe = new Response(response.status === 304 ? null : body, {
|
|
44
|
+
status: response.status, headers,
|
|
45
|
+
});
|
|
46
|
+
markResponseNonReplayable(safe);
|
|
47
|
+
return safe;
|
|
48
|
+
}
|