@bitkyc08/opencodex 2.72.0 → 2.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{App-Cf-WYstE.js → App-mHGa7dE1.js} +15 -15
- package/gui/dist/assets/{Tray-DkM0mULy.js → Tray-BxY_RYZg.js} +1 -1
- package/gui/dist/assets/{index-PWrwOfio.js → index-ur_BPgK1.js} +12 -12
- package/gui/dist/assets/{quota-summary-D-FKtrgr.js → quota-summary-D1Vq7N9H.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/cursor/native-exec.ts +28 -10
- package/src/adapters/devin/live-models.ts +5 -0
- package/src/adapters/kiro/reasoning.ts +2 -0
- package/src/adapters/openai-chat/messages.ts +4 -1
- package/src/adapters/openai-responses/passthrough.ts +12 -1
- package/src/adapters/openai-responses/reasoning.ts +15 -0
- package/src/bridge/internal.ts +12 -0
- package/src/cli/account-anthropic-threshold.ts +41 -0
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +32 -21
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +8 -4
- package/src/cli/index.ts +1 -1
- package/src/codex/catalog/build-entries.ts +6 -5
- package/src/codex/catalog/derive-entry.ts +3 -4
- package/src/codex/catalog/gather-capture.ts +2 -0
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/parsing.ts +4 -1
- package/src/codex/catalog/pinned-models.ts +3 -1
- package/src/codex/catalog/provider-models.ts +22 -2
- package/src/codex/data/roster-pinned-models.json +178 -1
- package/src/codex/model-entitlements.ts +1 -1
- package/src/codex/native-profile-store.ts +2 -1
- package/src/combos/resolve.ts +16 -1
- package/src/config/subagent-models.ts +30 -5
- package/src/generated/compatibility-version.json +95 -67
- package/src/generated/model-metadata.ts +4 -4
- package/src/lib/account-selection-events.ts +28 -0
- package/src/lib/errors.ts +7 -5
- package/src/lib/keyring-native.ts +132 -0
- package/src/lib/provider-outbound.ts +13 -0
- package/src/lib/retry-delay.ts +10 -0
- package/src/oauth/anthropic-account-threshold.ts +13 -0
- package/src/oauth/anthropic-routing.ts +223 -52
- package/src/oauth/index.ts +4 -2
- package/src/oauth/store.ts +45 -3
- package/src/oauth/types.ts +2 -0
- package/src/providers/api-key-resolve.ts +2 -4
- package/src/providers/codebuddy-models.ts +6 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/label.ts +20 -1
- package/src/providers/quota/account-cache.ts +2 -1
- package/src/providers/quota/anthropic-cooldown-recovery.ts +118 -0
- package/src/providers/quota/vendor-probes-oauth.ts +84 -57
- package/src/providers/quota.ts +45 -46
- package/src/providers/registry/entries-core.ts +3 -1
- package/src/providers/registry/entries-extended.ts +14 -1
- package/src/providers/registry/model-seeds.ts +7 -1
- package/src/server/claude-messages.ts +84 -27
- package/src/server/management/anthropic-account-threshold.ts +27 -0
- package/src/server/management/oauth-account-routes.ts +17 -4
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/messages-native-oauth.ts +20 -21
- package/src/server/messages-native.ts +39 -14
- package/src/server/relay-eager.ts +5 -2
- package/src/server/relay.ts +268 -82
- package/src/server/responses/adapter-continuation.ts +6 -0
- package/src/server/responses/adapter-delivery.ts +25 -7
- package/src/server/responses/adapter-dispatch.ts +31 -0
- package/src/server/responses/buffered-sse-json.ts +345 -0
- package/src/server/responses/core-codex-account.ts +4 -1
- package/src/server/responses/core-combo.ts +28 -0
- package/src/server/responses/core-lifetime.ts +4 -0
- package/src/server/responses/passthrough-delivery.ts +305 -10
- package/src/server/responses/passthrough-dispatch.ts +60 -25
- package/src/server/responses/passthrough-error.ts +3 -1
- package/src/server/responses/request-transport.ts +22 -5
- package/src/server/responses/terminal-error-redaction.ts +53 -0
- package/src/service/windows-taskxml.ts +20 -6
- package/src/service-manager-probe.ts +115 -6
- package/src/types/wire.ts +7 -0
- package/src/usage/expected-prices.ts +11 -0
|
@@ -1156,6 +1156,17 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
1156
1156
|
modelContextWindows: { "gpt-5.6-terra": 1_050_000 },
|
|
1157
1157
|
modelMaxOutputTokens: { "gpt-5.6-terra": 128_000 },
|
|
1158
1158
|
modelInputModalities: { "gpt-5.6-terra": ["text", "image"] },
|
|
1159
|
+
// Per-model wires from `tokenlab.accepted_request_formats` (GET /v1/models/{id}, 2026-09-30).
|
|
1160
|
+
// The provider stays on Chat, the released and end-to-end verified path, and every model
|
|
1161
|
+
// accepts it. Models that also declare Responses use it for Codex (Responses inbound) only,
|
|
1162
|
+
// so Chat and Anthropic clients skip a translation hop; an explicit modelAdapters entry wins.
|
|
1163
|
+
// Claude ids ride Anthropic Messages through the endpoint-bound pin in src/types/wire.ts.
|
|
1164
|
+
// gemini-3.8-flash declares Chat + Gemini native and stays on Chat. No delivery-policy
|
|
1165
|
+
// header is sent: the API key's own policy stays authoritative.
|
|
1166
|
+
modelWireDefaults: Object.fromEntries([
|
|
1167
|
+
"gpt-6-astra", "gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna", "grok-4.7",
|
|
1168
|
+
"deepseek-v4.1-flash", "deepseek-v4-pro", "kimi-k3", "glm-5.3",
|
|
1169
|
+
].map(id => [id, { wire: "openai-responses", inbound: ["responses"] }])),
|
|
1159
1170
|
modelDiscovery: {
|
|
1160
1171
|
path: "models",
|
|
1161
1172
|
query: { category: "chat" },
|
|
@@ -1342,7 +1353,7 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
1342
1353
|
featured: false,
|
|
1343
1354
|
dashboardUrl: "https://github.com/settings/copilot",
|
|
1344
1355
|
liveModels: true,
|
|
1345
|
-
models: ["gpt-4o", "gpt-4.1", "gpt-4.1-mini", "claude-sonnet-4", "gemini-2.5-pro", "gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6-sol", "gpt-6-luna"],
|
|
1356
|
+
models: ["gpt-4o", "gpt-4.1", "gpt-4.1-mini", "claude-sonnet-4", "gemini-2.5-pro", "gpt-5-mini", "gpt-5.3-codex", "gpt-5.4", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna"],
|
|
1346
1357
|
defaultModel: "gpt-4o",
|
|
1347
1358
|
// Copilot fronts a mixed-wire catalog: these models reject /chat/completions for
|
|
1348
1359
|
// real Codex-agent traffic (function tools + reasoning), so every inbound wire
|
|
@@ -1362,6 +1373,8 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
|
|
|
1362
1373
|
// 260923 preemptive: GPT-6 Sol/Luna ride Responses like every GPT-5.6/6 row above.
|
|
1363
1374
|
"gpt-6-sol": "openai-responses",
|
|
1364
1375
|
"gpt-6-luna": "openai-responses",
|
|
1376
|
+
// GPT-6.1 Sol: GA in Copilot 2026-09-29 (github.blog changelog); rides Responses like GPT-6 Sol.
|
|
1377
|
+
"gpt-6.1-sol": "openai-responses",
|
|
1365
1378
|
"grok-4.5": "openai-responses",
|
|
1366
1379
|
"grok-4.6": "openai-responses",
|
|
1367
1380
|
"mai-code-1.1-flash": "openai-responses",
|
|
@@ -164,8 +164,12 @@ export const OPENAI_API_GPT56_REASONING_EFFORTS = ["low", "medium", "high", "xhi
|
|
|
164
164
|
* GPT-6 Sol and Luna on the OpenAI API (released 2026-09-22,
|
|
165
165
|
* https://developers.openai.com/api/docs/changelog). Added 2026-09-23 ahead of live discovery; the
|
|
166
166
|
* API window is not published yet, so the rows mirror gpt-6-astra's 1,050,000 / 922,000 API seed.
|
|
167
|
+
*
|
|
168
|
+
* GPT-6.1 Sol (released 2026-09-29) publishes the same numbers on its own model page:
|
|
169
|
+
* 1,050,000 context, 922,000 max input, 128,000 max output, efforts low..max
|
|
170
|
+
* (https://developers.openai.com/api/docs/models/gpt-6.1-sol, checked 2026-09-30).
|
|
167
171
|
*/
|
|
168
|
-
export const OPENAI_GPT6_MODELS = ["gpt-6-sol", "gpt-6-luna"];
|
|
172
|
+
export const OPENAI_GPT6_MODELS = ["gpt-6.1-sol", "gpt-6-sol", "gpt-6-luna"];
|
|
169
173
|
/*
|
|
170
174
|
* Meta Model API (https://api.meta.ai/v1) — published ladder, deliberately NOT the
|
|
171
175
|
* house set. dev.meta.ai/docs/reasoning lists "none", "minimal", "low", "medium",
|
|
@@ -254,6 +258,8 @@ export const OPENROUTER_GPT56_CONTEXT_WINDOWS = {
|
|
|
254
258
|
// 260923 preemptive: GPT-6 Sol/Luna ahead of OpenRouter's own listing; same window as GPT-5.6.
|
|
255
259
|
"openai/gpt-6-sol": OPENROUTER_GPT56_CONTEXT_WINDOW,
|
|
256
260
|
"openai/gpt-6-luna": OPENROUTER_GPT56_CONTEXT_WINDOW,
|
|
261
|
+
// Live /api/v1/models on 2026-09-30: context_length 1,050,000, max_completion_tokens 128,000.
|
|
262
|
+
"openai/gpt-6.1-sol": OPENROUTER_GPT56_CONTEXT_WINDOW,
|
|
257
263
|
};
|
|
258
264
|
|
|
259
265
|
/**
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
resolveAdmissionModelScope,
|
|
15
15
|
} from "./admission-model-scope";
|
|
16
16
|
import { jsonUtf8Bytes } from "../lib/json-byte-size";
|
|
17
|
+
import { redactSecretString } from "../lib/redact";
|
|
17
18
|
import { sseFieldValue } from "../lib/sse-decoder";
|
|
18
19
|
import { enforceAnthropicImageLimits, sniffImageDimensions } from "../adapters/anthropic-image-guard";
|
|
19
20
|
import { normalizeAnthropicImages } from "../adapters/anthropic-image-normalize";
|
|
@@ -295,46 +296,55 @@ export interface PassthroughBodyGuard {
|
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
type PassthroughCloseReason = "terminal" | "client_cancel" | "body_stall" | "body_overflow";
|
|
299
|
+
type PassthroughFinalizeMeta = { closeReason: PassthroughCloseReason; terminalStatus?: "failed" };
|
|
298
300
|
|
|
299
301
|
/**
|
|
300
302
|
* Tap an Anthropic-vocabulary SSE stream for the request log (usage + terminal),
|
|
301
303
|
* bounding body occupancy: idle (silence-only, timed ONLY while a reader.read() is
|
|
302
304
|
* pending so downstream backpressure never counts as upstream inactivity) and a
|
|
303
|
-
* cumulative byte cap. On stall/overflow
|
|
304
|
-
*
|
|
305
|
-
* upstream reader — never a
|
|
305
|
+
* cumulative byte cap. On stall/overflow, or when the upstream read fails after the
|
|
306
|
+
* headers went out, it appends a protocol-compatible Anthropic `event: error` terminal
|
|
307
|
+
* frame after a blank-line boundary, closes, and cancels the upstream reader — never a
|
|
308
|
+
* total-wall-clock bound (slow-but-alive streams live).
|
|
306
309
|
* Exported for deterministic unit tests.
|
|
307
310
|
*/
|
|
308
311
|
export function tapAnthropicSseForLog(
|
|
309
312
|
upstream: ReadableStream<Uint8Array>,
|
|
310
313
|
logCtx: RequestLogContext,
|
|
311
|
-
finalize: (status: number, meta:
|
|
314
|
+
finalize: (status: number, meta: PassthroughFinalizeMeta) => void,
|
|
312
315
|
guard?: PassthroughBodyGuard,
|
|
313
316
|
): ReadableStream<Uint8Array> {
|
|
314
317
|
const decoder = new TextDecoder();
|
|
315
318
|
const encoder = new TextEncoder();
|
|
316
319
|
let buffer = "";
|
|
317
320
|
let usageAcc: Rec = {};
|
|
321
|
+
// message_stop or an upstream error event: the turn's own terminal already went through.
|
|
322
|
+
let terminalSeen = false;
|
|
323
|
+
const inspectFrame = (frame: string) => {
|
|
324
|
+
const dataLine = frame
|
|
325
|
+
.split("\n")
|
|
326
|
+
.map(l => sseFieldValue(l, "data"))
|
|
327
|
+
.filter((v): v is string => v !== null)
|
|
328
|
+
.join("");
|
|
329
|
+
if (!dataLine) return;
|
|
330
|
+
let data: unknown;
|
|
331
|
+
try { data = JSON.parse(dataLine); } catch { return; }
|
|
332
|
+
if (!isRec(data)) return;
|
|
333
|
+
if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
|
|
334
|
+
usageAcc = { ...usageAcc, ...data.message.usage };
|
|
335
|
+
} else if (data.type === "message_delta" && isRec(data.usage)) {
|
|
336
|
+
usageAcc = { ...usageAcc, ...data.usage };
|
|
337
|
+
} else if (data.type === "message_stop" || data.type === "error") {
|
|
338
|
+
terminalSeen = true;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
318
341
|
const inspect = (chunk: Uint8Array) => {
|
|
319
342
|
buffer += decoder.decode(chunk, { stream: true });
|
|
320
343
|
let sep: number;
|
|
321
344
|
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
322
345
|
const frame = buffer.slice(0, sep);
|
|
323
346
|
buffer = buffer.slice(sep + 2);
|
|
324
|
-
|
|
325
|
-
.split("\n")
|
|
326
|
-
.map(l => sseFieldValue(l, "data"))
|
|
327
|
-
.filter((v): v is string => v !== null)
|
|
328
|
-
.join("");
|
|
329
|
-
if (!dataLine) continue;
|
|
330
|
-
let data: unknown;
|
|
331
|
-
try { data = JSON.parse(dataLine); } catch { continue; }
|
|
332
|
-
if (!isRec(data)) continue;
|
|
333
|
-
if (data.type === "message_start" && isRec(data.message) && isRec(data.message.usage)) {
|
|
334
|
-
usageAcc = { ...usageAcc, ...data.message.usage };
|
|
335
|
-
} else if (data.type === "message_delta" && isRec(data.usage)) {
|
|
336
|
-
usageAcc = { ...usageAcc, ...data.usage };
|
|
337
|
-
}
|
|
347
|
+
inspectFrame(frame);
|
|
338
348
|
}
|
|
339
349
|
};
|
|
340
350
|
const reader = upstream.getReader();
|
|
@@ -345,13 +355,7 @@ export function tapAnthropicSseForLog(
|
|
|
345
355
|
const recordUsage = () => {
|
|
346
356
|
logCtx.usage = anthropicUsageToOcx(Object.keys(usageAcc).length > 0 ? usageAcc : undefined);
|
|
347
357
|
};
|
|
348
|
-
const
|
|
349
|
-
if (settled) return;
|
|
350
|
-
settled = true;
|
|
351
|
-
idle.cancel();
|
|
352
|
-
detachAbort();
|
|
353
|
-
recordUsage();
|
|
354
|
-
finalize(200, { closeReason });
|
|
358
|
+
const closeWithErrorFrame = (errType: string, message: string) => {
|
|
355
359
|
const payload = JSON.stringify({ type: "error", error: { type: errType, message } });
|
|
356
360
|
try {
|
|
357
361
|
// Leading blank line terminates any partial SSE block so the frame parses cleanly
|
|
@@ -359,6 +363,15 @@ export function tapAnthropicSseForLog(
|
|
|
359
363
|
tapController?.enqueue(encoder.encode(`\n\nevent: error\ndata: ${payload}\n\n`));
|
|
360
364
|
tapController?.close();
|
|
361
365
|
} catch { /* client already torn down */ }
|
|
366
|
+
};
|
|
367
|
+
const failBody = (closeReason: "body_stall" | "body_overflow", errType: string, message: string) => {
|
|
368
|
+
if (settled) return;
|
|
369
|
+
settled = true;
|
|
370
|
+
idle.cancel();
|
|
371
|
+
detachAbort();
|
|
372
|
+
recordUsage();
|
|
373
|
+
finalize(200, { closeReason });
|
|
374
|
+
closeWithErrorFrame(errType, message);
|
|
362
375
|
reader.cancel(new DOMException(message, closeReason === "body_stall" ? "TimeoutError" : "QuotaExceededError")).catch(() => {});
|
|
363
376
|
};
|
|
364
377
|
const idle = idleDeadline(guard?.stallMs ?? 0, () => {
|
|
@@ -427,12 +440,56 @@ export function tapAnthropicSseForLog(
|
|
|
427
440
|
controller.enqueue(value);
|
|
428
441
|
} catch (err) {
|
|
429
442
|
if (settled) return;
|
|
443
|
+
// Bun can settle a fetch body read before it dispatches the abort listeners
|
|
444
|
+
// (consumeForInspection in relay.ts): read the signal itself before calling this
|
|
445
|
+
// rejection an upstream failure.
|
|
446
|
+
if (guard?.reqSignal?.aborted) {
|
|
447
|
+
onClientAbort();
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
430
450
|
settled = true;
|
|
431
451
|
idle.cancel();
|
|
432
452
|
detachAbort();
|
|
453
|
+
// A read error can follow the last SSE block before its blank-line delimiter. Count
|
|
454
|
+
// that block before deciding how the turn ended, as the Responses relay does.
|
|
455
|
+
const terminalBeforeTail = terminalSeen;
|
|
456
|
+
const tail = buffer + decoder.decode();
|
|
457
|
+
buffer = "";
|
|
458
|
+
if (tail) inspectFrame(tail);
|
|
433
459
|
recordUsage();
|
|
434
|
-
|
|
435
|
-
|
|
460
|
+
if (isTranslatorBudgetExceededError(err)) {
|
|
461
|
+
// A local cap, not an upstream failure: the non-streaming native Messages fold
|
|
462
|
+
// maps this error to a 413 itself, so it still errors the stream.
|
|
463
|
+
finalize(200, { closeReason: "terminal" });
|
|
464
|
+
try { controller.error(err); } catch { /* torn down */ }
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (terminalSeen) {
|
|
468
|
+
// Only the transport trailer was lost; the client already has the turn's terminal.
|
|
469
|
+
// The Responses relay likewise reports a read error only without a seen terminal.
|
|
470
|
+
finalize(200, { closeReason: "terminal" });
|
|
471
|
+
try {
|
|
472
|
+
// An SSE parser drops an event that EOF cuts off before its blank line, so restore
|
|
473
|
+
// the delimiter when the terminal was only found in that unterminated tail.
|
|
474
|
+
if (!terminalBeforeTail) controller.enqueue(encoder.encode("\n\n"));
|
|
475
|
+
controller.close();
|
|
476
|
+
} catch { /* torn down */ }
|
|
477
|
+
reader.cancel(err).catch(() => {});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
// The upstream read failed after the 200 went out (a mid-stream socket reset). Log it
|
|
481
|
+
// the way the Responses relay does (onReadError): a truncated body is a failed turn,
|
|
482
|
+
// not a completed one. The client gets the same Anthropic error terminal as a stall,
|
|
483
|
+
// instead of a connection reset — or, on some Bun releases, a bare EOF that reads as
|
|
484
|
+
// a finished message.
|
|
485
|
+
const message = redactSecretString(`anthropic passthrough upstream stream failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
486
|
+
logCtx.transportPhase = "mid_stream";
|
|
487
|
+
logCtx.terminalSource = "synthetic";
|
|
488
|
+
logCtx.upstreamError = message.slice(0, 500);
|
|
489
|
+
if (logCtx.activeAttempt) logCtx.activeAttempt.streamAborted = true;
|
|
490
|
+
finalize(502, { terminalStatus: "failed", closeReason: "terminal" });
|
|
491
|
+
closeWithErrorFrame("api_error", message);
|
|
492
|
+
reader.cancel(err).catch(() => {});
|
|
436
493
|
}
|
|
437
494
|
},
|
|
438
495
|
cancel(reason) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { effectiveAnthropicAccountThreshold, parseAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
|
|
3
|
+
import { OAUTH_PROVIDERS } from "../../oauth";
|
|
4
|
+
import { setAnthropicAccountThreshold } from "../../oauth/store";
|
|
5
|
+
import { jsonResponse } from "../auth-cors";
|
|
6
|
+
import { readManagementJsonBodyOr } from "./body";
|
|
7
|
+
|
|
8
|
+
/** Account-owned policy writes share the auth-store lock, not a config/auth split transaction. */
|
|
9
|
+
export async function handleAnthropicAccountThreshold(req: Request, config: OcxConfig): Promise<Response> {
|
|
10
|
+
const body = await readManagementJsonBodyOr(req, {});
|
|
11
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
12
|
+
const fields = body as Record<string, unknown>;
|
|
13
|
+
const definition = OAUTH_PROVIDERS.anthropic;
|
|
14
|
+
const effectiveProvider = config.providers.anthropic
|
|
15
|
+
?? definition?.resolveProviderConfig?.(config) ?? definition?.providerConfig;
|
|
16
|
+
if (fields.provider !== "anthropic" || effectiveProvider?.authMode !== "oauth") {
|
|
17
|
+
return jsonResponse({ error: "account threshold requires Anthropic OAuth" }, 400);
|
|
18
|
+
}
|
|
19
|
+
if (typeof fields.accountId !== "string" || !fields.accountId.trim()) return jsonResponse({ error: "missing accountId" }, 400);
|
|
20
|
+
const threshold = parseAnthropicAccountThreshold(fields.threshold);
|
|
21
|
+
if (fields.threshold !== null && threshold === null) return jsonResponse({ error: "threshold must be an integer 0-100 or null" }, 400);
|
|
22
|
+
if (!await setAnthropicAccountThreshold(fields.accountId, threshold)) return jsonResponse({ error: "account not found" }, 404);
|
|
23
|
+
return jsonResponse({ ok: true, provider: "anthropic", accountId: fields.accountId,
|
|
24
|
+
autoSwitchThresholdOverride: threshold,
|
|
25
|
+
effectiveAutoSwitchThreshold: effectiveAnthropicAccountThreshold(config, { autoSwitchThresholdOverride: threshold ?? undefined }),
|
|
26
|
+
autoSwitchThreshold: effectiveAnthropicAccountThreshold(config) });
|
|
27
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { parseAnthropicModelRoutes, readAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
|
|
2
|
+
import { effectiveAnthropicAccountThreshold } from "../../oauth/anthropic-account-threshold";
|
|
3
|
+
import { handleAnthropicAccountThreshold } from "./anthropic-account-threshold";
|
|
2
4
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
3
5
|
import { readFileSync } from "node:fs";
|
|
4
6
|
import type { CatalogModel } from "../../codex/catalog";
|
|
@@ -396,7 +398,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
396
398
|
const { isGenericFailoverProvider, kiroAutoSelection } = await import("../../oauth/generic-account-failover");
|
|
397
399
|
const effectiveProvider = genericOAuthProviderConfig(provider, config);
|
|
398
400
|
const supportsPause = effectiveProvider !== undefined
|
|
399
|
-
&&
|
|
401
|
+
&& (provider === "anthropic" && effectiveProvider.authMode === "oauth"
|
|
402
|
+
|| isGenericFailoverProvider(provider, effectiveProvider));
|
|
400
403
|
const {
|
|
401
404
|
oauthAccountHealthFields,
|
|
402
405
|
projectOAuthAccountHealth,
|
|
@@ -417,6 +420,9 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
417
420
|
});
|
|
418
421
|
return { ...summary, ...oauthAccountHealthFields(provider, summary.id, health), quotaMode,
|
|
419
422
|
...(supportsPause ? { paused: full?.paused === true } : {}),
|
|
423
|
+
...(provider === "anthropic" && supportsPause ? { autoSwitchThresholdOverride: full?.autoSwitchThresholdOverride ?? null,
|
|
424
|
+
effectiveAutoSwitchThreshold: effectiveAnthropicAccountThreshold(config, full),
|
|
425
|
+
autoSwitchThreshold: effectiveAnthropicAccountThreshold(config) } : {}),
|
|
420
426
|
...(provider === "kiro" && full ? kiroAutoSelection(full) : {}) };
|
|
421
427
|
}),
|
|
422
428
|
};
|
|
@@ -492,6 +498,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
492
498
|
return jsonResponse({ ok: true, provider, activeAccountId: body.accountId });
|
|
493
499
|
}
|
|
494
500
|
|
|
501
|
+
if (url.pathname === "/api/oauth/accounts/auto-switch" && req.method === "PUT") return handleAnthropicAccountThreshold(req, config);
|
|
495
502
|
if (url.pathname === "/api/oauth/accounts/pause" && req.method === "PUT") {
|
|
496
503
|
const body = await readManagementJsonBodyOr(req, {});
|
|
497
504
|
if (!isPlainRecord(body)) return jsonResponse({ error: "body must be an object" }, 400);
|
|
@@ -504,7 +511,8 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
504
511
|
|
|
505
512
|
const { isGenericFailoverProvider } = await import("../../oauth/generic-account-failover");
|
|
506
513
|
const effectiveProvider = genericOAuthProviderConfig(provider, config);
|
|
507
|
-
if (!effectiveProvider || !
|
|
514
|
+
if (!effectiveProvider || !(provider === "anthropic" && effectiveProvider.authMode === "oauth"
|
|
515
|
+
|| isGenericFailoverProvider(provider, effectiveProvider))) {
|
|
508
516
|
return jsonResponse({ error: "account pause is not supported for this OAuth provider" }, 400);
|
|
509
517
|
}
|
|
510
518
|
|
|
@@ -513,8 +521,13 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
513
521
|
if (result.status === "not-found") return jsonResponse({ error: "account not found" }, 404);
|
|
514
522
|
|
|
515
523
|
if (result.activeAccountChanged) {
|
|
516
|
-
|
|
517
|
-
|
|
524
|
+
if (provider === "anthropic") {
|
|
525
|
+
const { resetAnthropicRoutingForManualSelection } = await import("../../oauth/anthropic-routing");
|
|
526
|
+
resetAnthropicRoutingForManualSelection(result.activeAccountId);
|
|
527
|
+
} else {
|
|
528
|
+
const { genericPoolKey, seedPoolRotationAccount } = await import("../../oauth/pool-kernel");
|
|
529
|
+
seedPoolRotationAccount(genericPoolKey(provider), result.activeAccountId);
|
|
530
|
+
}
|
|
518
531
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
519
532
|
const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
|
|
520
533
|
clearModelCache(provider);
|
|
@@ -323,6 +323,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
323
323
|
{ method: "PUT", path: "/api/oauth/accounts/active", module: "server/management/oauth-account-routes", mutates: true },
|
|
324
324
|
{ method: "PUT", path: "/api/oauth/accounts/alias", module: "server/management/oauth-account-routes", mutates: true },
|
|
325
325
|
{ method: "PUT", path: "/api/oauth/accounts/pause", module: "server/management/oauth-account-routes", mutates: true },
|
|
326
|
+
{ method: "PUT", path: "/api/oauth/accounts/auto-switch", module: "server/management/oauth-account-routes", mutates: true },
|
|
326
327
|
{ method: "PUT", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
|
|
327
328
|
{ method: "PUT", path: "/api/providers/keys/active", module: "server/management/oauth-account-routes", mutates: true },
|
|
328
329
|
{ method: "PUT", path: "/api/providers/keys/alias", module: "server/management/oauth-account-routes", mutates: true },
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Credential. The account is the one the existing OAuth selection chooses for this request, by
|
|
6
6
|
* the same steps the Responses pipeline's transport takes for an unpooled Anthropic OAuth route
|
|
7
|
-
* (`prepareResponsesTransport`): capture the committed selection, resolve
|
|
8
|
-
* access snapshot (
|
|
7
|
+
* (`prepareResponsesTransport`): capture the committed selection, resolve an eligible account's
|
|
8
|
+
* access snapshot (including pause/cooldown recovery), and commit that
|
|
9
9
|
* proposal against the captured selection so a concurrent manual switch wins. Nothing here picks
|
|
10
10
|
* an account of its own, and nothing runs at planning time: the planner and eligibility read
|
|
11
11
|
* config only, and this module is reached from the lane's dispatch alone.
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* Pools. A pooled account set (the opt-in Anthropic account pool, or two or more usable accounts,
|
|
14
14
|
* which turns on reactive 429 rotation) is declined before this lane is chosen
|
|
15
15
|
* (`oauth-account-pool`). Should one appear between that decision and dispatch, resolution fails
|
|
16
|
-
* closed rather than serve a pooled account without the pool's rotation and affinity.
|
|
16
|
+
* closed rather than serve a pooled account without the pool's rotation and affinity. This
|
|
17
|
+
* lane-change 409 is distinct from the shared local authentication/pause/cooldown refusals.
|
|
17
18
|
*
|
|
18
19
|
* Tool names. An OAuth request carries client tool names under the Claude OAuth prefix, as the
|
|
19
20
|
* adapter sends them; the answer's `tool_use` names are mapped back here, for exactly the names
|
|
@@ -21,10 +22,12 @@
|
|
|
21
22
|
*
|
|
22
23
|
* No token, account id or body content is logged or returned in an error message.
|
|
23
24
|
*/
|
|
24
|
-
import {
|
|
25
|
+
import type { OAuthAccessSnapshot } from "../oauth";
|
|
25
26
|
import {
|
|
26
27
|
commitAnthropicSelectionRouting,
|
|
27
28
|
getAnthropicPoolAccessSnapshot,
|
|
29
|
+
getAnthropicAccountHealthSnapshot,
|
|
30
|
+
resolveAnthropicDispatchAccountId,
|
|
28
31
|
hasAnthropicFailoverQuorum,
|
|
29
32
|
isAnthropicAccountPoolEnabled,
|
|
30
33
|
} from "../oauth/anthropic-routing";
|
|
@@ -72,17 +75,14 @@ function pooled(config: OcxConfig): boolean {
|
|
|
72
75
|
* selection could not be committed.
|
|
73
76
|
*/
|
|
74
77
|
export async function resolveNativeOAuthBinding(config: OcxConfig): Promise<NativeOAuthBinding> {
|
|
75
|
-
if (pooled(config)) throw new NativeOAuthSelectionChangedError();
|
|
76
|
-
let selection: Selection | null = captureOAuthAccountSelection(PROVIDER);
|
|
77
|
-
let candidate = await getValidAccessTokenSnapshot(PROVIDER);
|
|
78
78
|
for (let attempt = 0; attempt < MAX_SELECTION_ATTEMPTS; attempt++) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
const selection = captureOAuthAccountSelection(PROVIDER);
|
|
80
|
+
// Resolve eligibility before the lane guard: a local 401/403/429 must not turn
|
|
81
|
+
// into a generic lane-change 409 merely because it appeared during pacing.
|
|
82
|
+
const accountId = await resolveAnthropicDispatchAccountId(config);
|
|
83
|
+
if (pooled(config)) throw new NativeOAuthSelectionChangedError();
|
|
84
|
+
if (!selection) continue;
|
|
85
|
+
const candidate = await getAnthropicPoolAccessSnapshot(accountId);
|
|
86
86
|
const committed = await commitOAuthAccountSelection(PROVIDER, candidate.accountId, {
|
|
87
87
|
expectedSelection: selection,
|
|
88
88
|
expectedCredentialGeneration: candidate.generation,
|
|
@@ -93,14 +93,12 @@ export async function resolveNativeOAuthBinding(config: OcxConfig): Promise<Nati
|
|
|
93
93
|
config,
|
|
94
94
|
sessionKey: null,
|
|
95
95
|
expectedCredentialGeneration: candidate.generation,
|
|
96
|
-
}))
|
|
96
|
+
})) continue;
|
|
97
97
|
if (pooled(config)) break;
|
|
98
|
-
|
|
98
|
+
const binding = { selection: committed, snapshot: candidate };
|
|
99
|
+
if (nativeOAuthBindingIsCurrent(binding)) return binding;
|
|
99
100
|
}
|
|
100
|
-
//
|
|
101
|
-
selection = captureOAuthAccountSelection(PROVIDER);
|
|
102
|
-
if (!selection) break;
|
|
103
|
-
candidate = await getAnthropicPoolAccessSnapshot(selection.accountId);
|
|
101
|
+
// Re-read after a lost commit or a health change during credential resolution.
|
|
104
102
|
}
|
|
105
103
|
throw new NativeOAuthSelectionChangedError();
|
|
106
104
|
}
|
|
@@ -114,7 +112,8 @@ export function nativeOAuthBindingIsCurrent(binding: NativeOAuthBinding): boolea
|
|
|
114
112
|
const row = getAccountCredentialWithStatus(PROVIDER, binding.snapshot.accountId);
|
|
115
113
|
return selected?.accountId === binding.selection.accountId
|
|
116
114
|
&& selected?.revision === binding.selection.revision
|
|
117
|
-
&& !!row && !row.needsReauth && row.credential.expires > Date.now()
|
|
115
|
+
&& !!row && !row.paused && !row.needsReauth && row.credential.expires > Date.now()
|
|
116
|
+
&& !getAnthropicAccountHealthSnapshot(binding.snapshot.accountId)
|
|
118
117
|
&& credentialGeneration(row.credential) === binding.snapshot.generation;
|
|
119
118
|
}
|
|
120
119
|
|
|
@@ -58,9 +58,9 @@ import {
|
|
|
58
58
|
selectProactiveApiKeyTransport,
|
|
59
59
|
transientRetryPolicyFor,
|
|
60
60
|
} from "../providers/key-failover";
|
|
61
|
-
import { stampApiKeyAccountLabel
|
|
62
|
-
import { publicOAuthAuthenticationErrorMessage } from "../oauth";
|
|
63
|
-
import { hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
|
|
61
|
+
import { stampApiKeyAccountLabel } from "../providers/label";
|
|
62
|
+
import { OAuthAccountPausedError, OAuthLoginRequiredError, publicOAuthAuthenticationErrorMessage } from "../oauth";
|
|
63
|
+
import { AnthropicAccountCooldownError, formatAnthropicProviderForLog, hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
|
|
64
64
|
import { resolveProtocolSettings } from "../protocols/settings";
|
|
65
65
|
import { addProtocolEntryReason, markProtocolBlocked } from "../protocols/trace";
|
|
66
66
|
import type { OcxProviderTransport } from "../providers/xai-transport";
|
|
@@ -150,7 +150,7 @@ export interface HandleNativeMessagesOptions {
|
|
|
150
150
|
callerAnthropicBeta?: string | null;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
type FinishLog = (status: number, message?: string,
|
|
153
|
+
type FinishLog = (status: number, message?: string, meta?: FinalRequestLogMeta) => void;
|
|
154
154
|
|
|
155
155
|
/** Relay an upstream body unchanged, recording first output on its first non-empty chunk. */
|
|
156
156
|
function observeFirstChunk(body: ReadableStream<Uint8Array>, onFirst: () => void): ReadableStream<Uint8Array> {
|
|
@@ -311,10 +311,10 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
311
311
|
attemptHandle.seal(logCtx.accountLogLabel);
|
|
312
312
|
const { attempt } = attemptHandle;
|
|
313
313
|
const finalLog = createFinalRequestLog(logIds, logCtx);
|
|
314
|
-
const finishLog: FinishLog = (status, message,
|
|
314
|
+
const finishLog: FinishLog = (status, message, meta = { closeReason: "non_stream" }) => {
|
|
315
315
|
if (finalLog.finished()) return;
|
|
316
316
|
if (message) logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
317
|
-
finalLog.finish(status,
|
|
317
|
+
finalLog.finish(status, meta);
|
|
318
318
|
};
|
|
319
319
|
const bindUsage = (usage: OcxUsage | undefined) => {
|
|
320
320
|
if (!usage) return;
|
|
@@ -328,6 +328,16 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
328
328
|
finishLog(status, safeMessage);
|
|
329
329
|
return anthropicErrorResponse(status, safeMessage, type, code);
|
|
330
330
|
};
|
|
331
|
+
const localOAuthFailure = (error: unknown): Response | null => {
|
|
332
|
+
if (error instanceof AnthropicAccountCooldownError) {
|
|
333
|
+
const response = fail(429, error.message, "rate_limit_error");
|
|
334
|
+
if (error.retryAfterSeconds !== null) response.headers.set("retry-after", String(error.retryAfterSeconds));
|
|
335
|
+
return response;
|
|
336
|
+
}
|
|
337
|
+
if (error instanceof OAuthAccountPausedError) return fail(403, publicOAuthAuthenticationErrorMessage(error), "permission_error");
|
|
338
|
+
if (error instanceof OAuthLoginRequiredError) return fail(401, publicOAuthAuthenticationErrorMessage(error), "authentication_error");
|
|
339
|
+
return null;
|
|
340
|
+
};
|
|
331
341
|
|
|
332
342
|
try {
|
|
333
343
|
await prepareNativeBody(body, req.signal);
|
|
@@ -366,6 +376,8 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
366
376
|
cleanupAbort();
|
|
367
377
|
upstream.abort();
|
|
368
378
|
if (req.signal.aborted) return fail(499, "Client cancelled request", "api_error");
|
|
379
|
+
const localRefusal = localOAuthFailure(error);
|
|
380
|
+
if (localRefusal) return localRefusal;
|
|
369
381
|
if (error instanceof NativeOAuthSelectionChangedError) return fail(409, error.message, "api_error");
|
|
370
382
|
return fail(401, publicOAuthAuthenticationErrorMessage(error), "authentication_error");
|
|
371
383
|
}
|
|
@@ -376,7 +388,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
376
388
|
}
|
|
377
389
|
let activeProvider: OcxProviderConfig = oauthBinding ? oauthProvider(oauthBinding) : route.provider;
|
|
378
390
|
stampApiKeyAccountLabel(logCtx, route.providerName, activeProvider);
|
|
379
|
-
if (oauthBinding)
|
|
391
|
+
if (oauthBinding) logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
|
|
380
392
|
const spendTracker = attachRequestSpendTracker(req, logCtx);
|
|
381
393
|
let activeRequest: AnthropicMessagesPassthroughRequest;
|
|
382
394
|
let retainedRequestBytes = 0;
|
|
@@ -460,14 +472,17 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
460
472
|
if (oauthBinding) {
|
|
461
473
|
// The OAuth twin of the key check below: re-resolve through the same selection
|
|
462
474
|
// owner when the committed account or its credential moved since the build.
|
|
463
|
-
|
|
475
|
+
for (let attempt = 0; !nativeOAuthBindingIsCurrent(oauthBinding); attempt++) {
|
|
476
|
+
if (attempt >= 3) throw new NativeOAuthSelectionChangedError();
|
|
464
477
|
try {
|
|
465
478
|
oauthBinding = await resolveNativeOAuthBinding(config);
|
|
466
|
-
} catch {
|
|
479
|
+
} catch (error) {
|
|
480
|
+
if (error instanceof OAuthAccountPausedError || error instanceof OAuthLoginRequiredError
|
|
481
|
+
|| error instanceof AnthropicAccountCooldownError) throw error;
|
|
467
482
|
throw new NativeOAuthSelectionChangedError();
|
|
468
483
|
}
|
|
469
484
|
rebuildFor(oauthProvider(oauthBinding));
|
|
470
|
-
|
|
485
|
+
logCtx.provider = formatAnthropicProviderForLog(route.providerName, oauthBinding.snapshot.accountId, config);
|
|
471
486
|
}
|
|
472
487
|
} else if (!providerApiKeySelectionIsCurrent(config, route.providerName, activeProvider)) {
|
|
473
488
|
const current = resolveCurrentProviderApiKeyTransport(config, route.providerName, activeProvider);
|
|
@@ -569,6 +584,8 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
569
584
|
finishLog(429);
|
|
570
585
|
return refusal;
|
|
571
586
|
}
|
|
587
|
+
const localRefusal = localOAuthFailure(sendError);
|
|
588
|
+
if (localRefusal) return localRefusal;
|
|
572
589
|
if (sendError instanceof NativeOAuthSelectionChangedError) return fail(409, sendError.message, "api_error");
|
|
573
590
|
if (sendError instanceof NativeOpaqueStateRefusal) {
|
|
574
591
|
logCtx.errorCode = "unsupported_feature";
|
|
@@ -597,7 +614,9 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
597
614
|
|
|
598
615
|
const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
|
|
599
616
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
600
|
-
|
|
617
|
+
// `upstream` follows req.signal and is also what shutdown and turn release abort, so both
|
|
618
|
+
// the client leaving and this lane's own abort read as a cancel, not as a failed stream.
|
|
619
|
+
const bodyGuard = resolvePassthroughBodyGuard(config, upstream.signal);
|
|
601
620
|
const observed = logIds ? observeFirstChunk(response.body, () => recordFirstOutput(logCtx, logIds.start)) : response.body;
|
|
602
621
|
const renamed = activeRequest.oauthToolNames
|
|
603
622
|
? restoreOAuthToolNamesInSse(observed, activeRequest.oauthToolNames, translatorBudget)
|
|
@@ -609,7 +628,7 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
609
628
|
try {
|
|
610
629
|
cleanupAbort();
|
|
611
630
|
bindUsage(logCtx.usage);
|
|
612
|
-
finishLog(status, undefined, meta
|
|
631
|
+
finishLog(status, undefined, meta);
|
|
613
632
|
if (meta.closeReason !== "terminal") upstream.abort();
|
|
614
633
|
} finally {
|
|
615
634
|
releaseStreamTurn();
|
|
@@ -621,9 +640,10 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
621
640
|
});
|
|
622
641
|
}
|
|
623
642
|
// A non-streaming caller whose upstream streamed anyway: fold the stream into one message.
|
|
624
|
-
const tapState: { closeReason?: FinalRequestLogMeta["closeReason"] } = {};
|
|
643
|
+
const tapState: { closeReason?: FinalRequestLogMeta["closeReason"]; meta?: FinalRequestLogMeta } = {};
|
|
625
644
|
const tapped = tapAnthropicSseForLog(source, logCtx, (_status, meta) => {
|
|
626
645
|
tapState.closeReason = meta.closeReason;
|
|
646
|
+
tapState.meta = meta;
|
|
627
647
|
}, bodyGuard);
|
|
628
648
|
try {
|
|
629
649
|
const message = await collectAnthropicMessage(tapped, requestedModel, translatorBudget);
|
|
@@ -634,7 +654,12 @@ export async function handleNativeMessages(options: HandleNativeMessagesOptions)
|
|
|
634
654
|
bindUsage(logCtx.usage);
|
|
635
655
|
if (message.type === "error") {
|
|
636
656
|
const error = isRec(message.error) ? message.error : {};
|
|
637
|
-
|
|
657
|
+
const text = typeof error.message === "string" ? error.message : "upstream stream failed";
|
|
658
|
+
// The tap's own verdict (a reset, a stall, the byte cap) closes this row with the same meta
|
|
659
|
+
// as the streaming lane's row; a plain upstream error event keeps the non_stream row.
|
|
660
|
+
const tapMeta = tapState.meta;
|
|
661
|
+
if (tapMeta && (tapMeta.terminalStatus || tapMeta.closeReason !== "terminal")) finishLog(502, text, tapMeta);
|
|
662
|
+
return fail(502, text, "api_error");
|
|
638
663
|
}
|
|
639
664
|
finishLog(200);
|
|
640
665
|
return Response.json(message);
|
|
@@ -88,6 +88,8 @@ export type EagerRelayOptions = {
|
|
|
88
88
|
upstreamError?: string;
|
|
89
89
|
/** Optional client-facing hint policy; inspection retains original frames. */
|
|
90
90
|
terminalBoundary?: CodexSafetyBufferingFilterOptions;
|
|
91
|
+
/** Mask the selected outbound credential in synthetic failures. */
|
|
92
|
+
maskCredential?: (text: string) => string;
|
|
91
93
|
/** Injectable clock for tests. */
|
|
92
94
|
now?: () => number;
|
|
93
95
|
};
|
|
@@ -123,7 +125,7 @@ export function relaySseEagerBounded(
|
|
|
123
125
|
?? (hooks.rewritePayload ? payloadRewriteAsBlockRewrite(hooks.rewritePayload) : undefined);
|
|
124
126
|
const encodeFailedTail = (error: unknown): Uint8Array | null => {
|
|
125
127
|
try {
|
|
126
|
-
return failedTailFrame(terminalEncoder, error);
|
|
128
|
+
return failedTailFrame(terminalEncoder, error, opts?.maskCredential);
|
|
127
129
|
} catch {
|
|
128
130
|
return null;
|
|
129
131
|
}
|
|
@@ -140,7 +142,7 @@ export function relaySseEagerBounded(
|
|
|
140
142
|
return encodeFailedTail(error);
|
|
141
143
|
}
|
|
142
144
|
try {
|
|
143
|
-
return refusalFailedTailFrame(terminalEncoder, refusalMessage, refusalCode);
|
|
145
|
+
return refusalFailedTailFrame(terminalEncoder, refusalMessage, refusalCode, opts?.maskCredential);
|
|
144
146
|
} catch {
|
|
145
147
|
return null;
|
|
146
148
|
}
|
|
@@ -335,6 +337,7 @@ export function relaySseEagerBounded(
|
|
|
335
337
|
terminalEncoder,
|
|
336
338
|
upstreamError,
|
|
337
339
|
terminalBoundary.upstreamRefusalCode(),
|
|
340
|
+
opts?.maskCredential,
|
|
338
341
|
);
|
|
339
342
|
queuedBytes += upstreamErrorFrame.byteLength + terminalSentinel.byteLength;
|
|
340
343
|
try {
|