dsh-subscriptions 0.1.2 → 0.1.4
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/README.md +92 -10
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- package/lib/client/ExternalUsageCards.d.ts +11 -1
- package/lib/client/ExternalUsageCards.js +18 -3
- package/lib/client/ProviderAccountManager.js +34 -41
- package/lib/client/ProviderModelEditor.js +5 -9
- package/lib/client/SpeedSelect.js +8 -7
- package/lib/client/SubscriptionUsageBadge.d.ts +19 -2
- package/lib/client/SubscriptionUsageBadge.js +65 -9
- package/lib/client/SubscriptionsSection.d.ts +22 -0
- package/lib/client/SubscriptionsSection.js +128 -38
- package/lib/client/UsageBadgeDisplaySetting.js +6 -10
- package/lib/client/UsageMeter.js +11 -6
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +60 -0
- package/lib/client/locales.js +62 -2
- package/lib/client/provider-settings-styles.d.ts +3 -0
- package/lib/client/provider-settings-styles.js +206 -0
- package/lib/client/reset-action.d.ts +14 -0
- package/lib/client/reset-action.js +15 -0
- package/lib/client/reset-credits-view.d.ts +53 -0
- package/lib/client/reset-credits-view.js +103 -0
- package/lib/client/usage-badge-preferences.d.ts +7 -2
- package/lib/client/usage-badge-preferences.js +3 -2
- package/lib/client/usage-pace.d.ts +14 -1
- package/lib/client/usage-pace.js +14 -1
- package/lib/client.js +1036 -334
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +1120 -602
- package/lib/providers/account-preferences.js +3 -2
- package/lib/providers/catalog-store.d.ts +3 -2
- package/lib/providers/catalog-store.js +57 -26
- package/lib/providers/claude.d.ts +22 -3
- package/lib/providers/claude.js +113 -15
- package/lib/providers/codex.d.ts +15 -2
- package/lib/providers/codex.js +72 -4
- package/lib/providers/common.d.ts +25 -0
- package/lib/providers/common.js +22 -18
- package/lib/providers/copilot.d.ts +2 -1
- package/lib/providers/copilot.js +10 -3
- package/lib/providers/grok.d.ts +2 -2
- package/lib/providers/grok.js +2 -2
- package/lib/providers/minimax-usage.d.ts +22 -1
- package/lib/providers/minimax-usage.js +37 -7
- package/lib/providers/pool-health.d.ts +2 -0
- package/lib/providers/pool-health.js +9 -0
- package/lib/providers/pool-usage.d.ts +13 -1
- package/lib/providers/pool-usage.js +39 -7
- package/lib/providers/pool.d.ts +7 -5
- package/lib/providers/pool.js +89 -12
- package/lib/providers/rate-limit.d.ts +0 -14
- package/lib/providers/rate-limit.js +0 -26
- package/lib/providers/replay.d.ts +4 -0
- package/lib/providers/replay.js +50 -0
- package/lib/providers/reset-redemption.d.ts +51 -0
- package/lib/providers/reset-redemption.js +102 -0
- package/lib/translate/anthropic.d.ts +21 -2
- package/lib/translate/anthropic.js +35 -11
- package/lib/translate/antigravity.js +13 -6
- package/lib/translate/chat-completions.js +1 -1
- package/lib/translate/resolved.d.ts +21 -0
- package/lib/translate/resolved.js +63 -3
- package/lib/translate/responses.js +8 -6
- package/lib/translate/sse.js +8 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import http, { createServer } from "node:http";
|
|
|
5
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
6
6
|
import { getDefaultAutoSelectFamilyAttemptTimeout, setDefaultAutoSelectFamilyAttemptTimeout } from "node:net";
|
|
7
7
|
import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
8
|
-
import { basename, dirname, join } from "node:path";
|
|
8
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
9
9
|
import { execFileSync } from "node:child_process";
|
|
10
10
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
@@ -97,12 +97,12 @@ async function listen(handler, spec) {
|
|
|
97
97
|
for (const host of hosts) {
|
|
98
98
|
const server = createServer(handler);
|
|
99
99
|
try {
|
|
100
|
-
await new Promise((resolve$
|
|
100
|
+
await new Promise((resolve$2, reject) => {
|
|
101
101
|
const onError = (error) => reject(error);
|
|
102
102
|
server.once("error", onError);
|
|
103
103
|
server.listen(port, host, () => {
|
|
104
104
|
server.removeListener("error", onError);
|
|
105
|
-
resolve$
|
|
105
|
+
resolve$2();
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
const address = server.address();
|
|
@@ -169,8 +169,8 @@ var OAuthFlowManager = class {
|
|
|
169
169
|
const timeoutMs = spec.timeoutMs ?? DEFAULT_FLOW_TIMEOUT_MS;
|
|
170
170
|
let resolveCode;
|
|
171
171
|
let rejectCode;
|
|
172
|
-
const codePromise = new Promise((resolve$
|
|
173
|
-
resolveCode = resolve$
|
|
172
|
+
const codePromise = new Promise((resolve$2, reject) => {
|
|
173
|
+
resolveCode = resolve$2;
|
|
174
174
|
rejectCode = reject;
|
|
175
175
|
});
|
|
176
176
|
let settled = false;
|
|
@@ -280,14 +280,14 @@ const DEFAULT_INTERVAL_SEC = 5;
|
|
|
280
280
|
const DEFAULT_EXPIRES_IN_SEC = 900;
|
|
281
281
|
/** Sleep for `ms`, rejecting early when the signal aborts. */
|
|
282
282
|
function sleep$1(ms, signal) {
|
|
283
|
-
return new Promise((resolve$
|
|
283
|
+
return new Promise((resolve$2, reject) => {
|
|
284
284
|
if (signal.aborted) {
|
|
285
285
|
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("aborted"));
|
|
286
286
|
return;
|
|
287
287
|
}
|
|
288
288
|
const timer = setTimeout(() => {
|
|
289
289
|
signal.removeEventListener("abort", onAbort);
|
|
290
|
-
resolve$
|
|
290
|
+
resolve$2();
|
|
291
291
|
}, ms);
|
|
292
292
|
timer.unref();
|
|
293
293
|
const onAbort = () => {
|
|
@@ -349,8 +349,8 @@ var DeviceFlowManager = class {
|
|
|
349
349
|
const controller = new AbortController();
|
|
350
350
|
let resolveToken;
|
|
351
351
|
let rejectToken;
|
|
352
|
-
const tokenPromise = new Promise((resolve$
|
|
353
|
-
resolveToken = resolve$
|
|
352
|
+
const tokenPromise = new Promise((resolve$2, reject) => {
|
|
353
|
+
resolveToken = resolve$2;
|
|
354
354
|
rejectToken = reject;
|
|
355
355
|
});
|
|
356
356
|
tokenPromise.catch(() => void 0);
|
|
@@ -602,6 +602,81 @@ async function refreshClaudeSynced(session, doRefresh) {
|
|
|
602
602
|
return next;
|
|
603
603
|
}
|
|
604
604
|
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/providers/reset-redemption.ts
|
|
607
|
+
/**
|
|
608
|
+
* Manual-only redemption with single-use tickets. An ambiguous submission parks
|
|
609
|
+
* the account to prevent an immediate blind retry. This block is process-local,
|
|
610
|
+
* not a durable rate limit: restarting the host clears it. Re-check credits and
|
|
611
|
+
* limits in Codex before retrying; a lost success response may have spent a
|
|
612
|
+
* credit already, and another submission can spend another credit.
|
|
613
|
+
* Rationale: [manual reset note](../../.agents/notes/implemented/provider/2026-10-04-codex-manual-reset.md).
|
|
614
|
+
*/
|
|
615
|
+
var ResetRedemption = class {
|
|
616
|
+
tickets = /* @__PURE__ */ new Map();
|
|
617
|
+
busy = /* @__PURE__ */ new Set();
|
|
618
|
+
uncertain = /* @__PURE__ */ new Set();
|
|
619
|
+
constructor(read, consume, invalidate, canonical) {
|
|
620
|
+
this.read = read;
|
|
621
|
+
this.consume = consume;
|
|
622
|
+
this.invalidate = invalidate;
|
|
623
|
+
this.canonical = canonical;
|
|
624
|
+
}
|
|
625
|
+
eligible(usage) {
|
|
626
|
+
const weekly = usage.windows?.find((w) => w.kind === "weekly" && w.scope === void 0);
|
|
627
|
+
if (!weekly || !Number.isFinite(weekly.usedPercent) || weekly.usedPercent < 80 || weekly.resetsAt === void 0 || weekly.resetsAt <= Date.now()) throw new Error("Reset blocked: fresh weekly usage must be at least 80%.");
|
|
628
|
+
if (usage.resetCreditsError) throw new Error("Reset credits could not be verified.");
|
|
629
|
+
return weekly.usedPercent;
|
|
630
|
+
}
|
|
631
|
+
async prepare(account, signal) {
|
|
632
|
+
const who = await this.canonical(account, signal);
|
|
633
|
+
if (this.busy.has(who) || this.uncertain.has(who)) throw new Error("Reset blocked: an operation is pending or its outcome is uncertain. Check Codex before retrying.");
|
|
634
|
+
const usage = await this.read(account, signal);
|
|
635
|
+
if (this.busy.has(who) || this.uncertain.has(who)) throw new Error("Reset blocked: an operation is pending or its outcome is uncertain. Check Codex before retrying.");
|
|
636
|
+
const weeklyUsedPercent = this.eligible(usage);
|
|
637
|
+
const credit = usage.resetCredits?.filter((c) => c.id && (c.expiresAt === void 0 || c.expiresAt > Date.now())).sort((a, b) => (a.expiresAt ?? Infinity) - (b.expiresAt ?? Infinity))[0];
|
|
638
|
+
if (!credit?.id) throw new Error("No available reset credit.");
|
|
639
|
+
for (const [key, value] of this.tickets) if (value.account === who || value.expiresAt <= Date.now()) this.tickets.delete(key);
|
|
640
|
+
const ticket = randomUUID();
|
|
641
|
+
const expiresAt = Date.now() + 6e4;
|
|
642
|
+
this.tickets.set(ticket, {
|
|
643
|
+
account: who,
|
|
644
|
+
creditId: credit.id,
|
|
645
|
+
expiresAt
|
|
646
|
+
});
|
|
647
|
+
return {
|
|
648
|
+
ticket,
|
|
649
|
+
expiresAt,
|
|
650
|
+
weeklyUsedPercent,
|
|
651
|
+
...credit.expiresAt === void 0 ? {} : { creditExpiresAt: credit.expiresAt }
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
async redeem(account, ticket, signal) {
|
|
655
|
+
const who = await this.canonical(account, signal);
|
|
656
|
+
const pending = this.tickets.get(ticket);
|
|
657
|
+
this.tickets.delete(ticket);
|
|
658
|
+
if (!pending || pending.account !== who || pending.expiresAt <= Date.now()) throw new Error("Confirmation expired. Start again.");
|
|
659
|
+
if (this.busy.has(who) || this.uncertain.has(who)) throw new Error("Reset blocked: operation pending or uncertain.");
|
|
660
|
+
this.busy.add(who);
|
|
661
|
+
try {
|
|
662
|
+
const usage = await this.read(account, signal);
|
|
663
|
+
if (pending.expiresAt <= Date.now()) throw new Error("Confirmation expired. Start again.");
|
|
664
|
+
this.eligible(usage);
|
|
665
|
+
if (!usage.resetCredits?.some((c) => c.id === pending.creditId && (c.expiresAt === void 0 || c.expiresAt > Date.now()))) throw new Error("The selected reset is no longer available.");
|
|
666
|
+
this.uncertain.add(who);
|
|
667
|
+
try {
|
|
668
|
+
await this.consume(account, pending.creditId, randomUUID(), signal);
|
|
669
|
+
} catch {
|
|
670
|
+
throw new Error("Reset outcome could not be confirmed. Do not retry or restart to bypass this block; verify credits and limits in Codex first.");
|
|
671
|
+
}
|
|
672
|
+
this.uncertain.delete(who);
|
|
673
|
+
} finally {
|
|
674
|
+
this.invalidate(who);
|
|
675
|
+
this.busy.delete(who);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
605
680
|
//#endregion
|
|
606
681
|
//#region src/auth/jwt.ts
|
|
607
682
|
/** Minimal JWT payload decoding for claims extraction (no signature verification). */
|
|
@@ -905,7 +980,7 @@ async function writeStore(store, path) {
|
|
|
905
980
|
* A chain is dropped once nothing is queued behind it, so the map holds an
|
|
906
981
|
* entry only while writes are in flight.
|
|
907
982
|
*/
|
|
908
|
-
const writeChains = /* @__PURE__ */ new Map();
|
|
983
|
+
const writeChains$1 = /* @__PURE__ */ new Map();
|
|
909
984
|
/**
|
|
910
985
|
* Run one read-modify-write of a store path after every write already queued
|
|
911
986
|
* for it. Callers join the chain synchronously, so call order is write order.
|
|
@@ -913,14 +988,14 @@ const writeChains = /* @__PURE__ */ new Map();
|
|
|
913
988
|
* @param action - the read-modify-write to run.
|
|
914
989
|
* @returns whatever `action` returns.
|
|
915
990
|
*/
|
|
916
|
-
async function serialize(path, action) {
|
|
917
|
-
const next = (writeChains.get(path) ?? Promise.resolve()).then(action, action);
|
|
991
|
+
async function serialize$1(path, action) {
|
|
992
|
+
const next = (writeChains$1.get(path) ?? Promise.resolve()).then(action, action);
|
|
918
993
|
const tail = next.then(() => void 0, () => void 0);
|
|
919
|
-
writeChains.set(path, tail);
|
|
994
|
+
writeChains$1.set(path, tail);
|
|
920
995
|
try {
|
|
921
996
|
return await next;
|
|
922
997
|
} finally {
|
|
923
|
-
if (writeChains.get(path) === tail) writeChains.delete(path);
|
|
998
|
+
if (writeChains$1.get(path) === tail) writeChains$1.delete(path);
|
|
924
999
|
}
|
|
925
1000
|
}
|
|
926
1001
|
/**
|
|
@@ -968,7 +1043,7 @@ async function getAccountSession(provider, account, path = authFilePath()) {
|
|
|
968
1043
|
*/
|
|
969
1044
|
async function saveAccountSession(provider, account, session, path = authFilePath()) {
|
|
970
1045
|
assertSessionShape(provider, account, session);
|
|
971
|
-
return serialize(path, async () => {
|
|
1046
|
+
return serialize$1(path, async () => {
|
|
972
1047
|
const store = await loadStore(path);
|
|
973
1048
|
const entry = store[provider];
|
|
974
1049
|
if (entry !== void 0) {
|
|
@@ -994,7 +1069,7 @@ async function saveAccountSession(provider, account, session, path = authFilePat
|
|
|
994
1069
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
995
1070
|
*/
|
|
996
1071
|
async function deleteAccountSession(provider, account, path = authFilePath()) {
|
|
997
|
-
return serialize(path, async () => {
|
|
1072
|
+
return serialize$1(path, async () => {
|
|
998
1073
|
const store = await loadStore(path);
|
|
999
1074
|
const entry = store[provider];
|
|
1000
1075
|
if (entry === void 0) return;
|
|
@@ -1018,7 +1093,7 @@ async function deleteAccountSession(provider, account, path = authFilePath()) {
|
|
|
1018
1093
|
* @param path - store file path; defaults to {@link authFilePath}.
|
|
1019
1094
|
*/
|
|
1020
1095
|
async function setDefaultAccount(provider, account, path = authFilePath()) {
|
|
1021
|
-
return serialize(path, async () => {
|
|
1096
|
+
return serialize$1(path, async () => {
|
|
1022
1097
|
const store = await loadStore(path);
|
|
1023
1098
|
const entry = store[provider];
|
|
1024
1099
|
if (entry !== void 0) account = resolveAccount(entry, account);
|
|
@@ -1033,7 +1108,37 @@ async function setDefaultAccount(provider, account, path = authFilePath()) {
|
|
|
1033
1108
|
|
|
1034
1109
|
//#endregion
|
|
1035
1110
|
//#region src/providers/minimax-usage.ts
|
|
1036
|
-
/**
|
|
1111
|
+
/**
|
|
1112
|
+
* Used percentage from the legacy counts, read as the official CLI does when no
|
|
1113
|
+
* explicit percentage exists: the count is the quota remaining. Undefined
|
|
1114
|
+
* unless the total is a positive finite number, so a zero total never yields a
|
|
1115
|
+
* bar.
|
|
1116
|
+
*/
|
|
1117
|
+
function usedFromCounts(total, count) {
|
|
1118
|
+
return typeof total === "number" && Number.isFinite(total) && total > 0 && typeof count === "number" ? (1 - count / total) * 100 : void 0;
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* Read MiniMax subscription quota (M Plan and Token Plan), not the balance API
|
|
1122
|
+
* that `sk-api-*` pay-as-you-go keys use.
|
|
1123
|
+
*
|
|
1124
|
+
* Each `model_remains` row can yield an interval window and a weekly window.
|
|
1125
|
+
* A window needs a finite share: the explicit `*_remaining_percent` (the weekly
|
|
1126
|
+
* one scaled by `weekly_boost_permille`), else the legacy counts. Time-based
|
|
1127
|
+
* plans report zero counts beside the percentage for standard models, so the
|
|
1128
|
+
* percentage stands on its own and a zero total does not hide the window.
|
|
1129
|
+
* Windows that cannot be a finite bar are omitted rather than approximated:
|
|
1130
|
+
* status 3 (unlimited, or no quota in the plan), a weekly pool boosted past
|
|
1131
|
+
* 100% remaining, a percentage outside 0-100, and a row with neither a
|
|
1132
|
+
* percentage nor positive counts. Start and end times are kept only when both
|
|
1133
|
+
* are valid millisecond bounds.
|
|
1134
|
+
* @param key - subscription key for the region; never copied into a result or error.
|
|
1135
|
+
* @param region - `global` or `cn`, which selects the fixed quota host.
|
|
1136
|
+
* @param http - fetch implementation (injectable for tests).
|
|
1137
|
+
* @param signal - caller cancellation from the RPC transport.
|
|
1138
|
+
* @returns the finite windows in the order the API lists them.
|
|
1139
|
+
* @throws when the key is pay-as-you-go, the endpoint or its business status
|
|
1140
|
+
* fails, the body has no `model_remains`, or no window is a finite bar.
|
|
1141
|
+
*/
|
|
1037
1142
|
async function fetchMiniMaxUsage(key, region, http$1 = fetch, signal) {
|
|
1038
1143
|
if (key.startsWith("sk-api-")) throw new Error("MiniMax requires a subscription key, not a pay-as-you-go key");
|
|
1039
1144
|
const response = await http$1(`https://www.minimax.${region === "cn" ? "cn" : "io"}/v1/token_plan/remains`, {
|
|
@@ -1066,10 +1171,7 @@ async function fetchMiniMaxUsage(key, region, http$1 = fetch, signal) {
|
|
|
1066
1171
|
const remaining = row[`${prefix}_remaining_percent`];
|
|
1067
1172
|
const boost = prefix === "current_weekly" ? row.weekly_boost_permille ?? 1e3 : 1e3;
|
|
1068
1173
|
if (typeof boost !== "number" || !Number.isFinite(boost) || boost <= 0) continue;
|
|
1069
|
-
const
|
|
1070
|
-
const count = row[`${prefix}_usage_count`];
|
|
1071
|
-
if (typeof total !== "number" || !Number.isFinite(total) || total <= 0) continue;
|
|
1072
|
-
const usedPercent = typeof remaining === "number" ? 100 - remaining * boost / 1e3 : typeof total === "number" && total > 0 && typeof count === "number" ? (1 - count / total) * 100 : void 0;
|
|
1174
|
+
const usedPercent = typeof remaining === "number" ? 100 - remaining * boost / 1e3 : usedFromCounts(row[`${prefix}_total_count`], row[`${prefix}_usage_count`]);
|
|
1073
1175
|
if (usedPercent === void 0 || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) continue;
|
|
1074
1176
|
const start = row[startKey];
|
|
1075
1177
|
const end = row[endKey];
|
|
@@ -1340,6 +1442,8 @@ const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
|
|
|
1340
1442
|
"cursorSetSettings",
|
|
1341
1443
|
"image",
|
|
1342
1444
|
"video",
|
|
1445
|
+
"prepareReset",
|
|
1446
|
+
"consumeReset",
|
|
1343
1447
|
"speed",
|
|
1344
1448
|
"setSpeed",
|
|
1345
1449
|
"modelDefaults",
|
|
@@ -1619,6 +1723,13 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
1619
1723
|
if (payload === null || typeof payload !== "object") throw new BadRequest("payload must be an object");
|
|
1620
1724
|
await cursor.setSettings(payload.settings);
|
|
1621
1725
|
return ok({ ok: true });
|
|
1726
|
+
case "prepareReset":
|
|
1727
|
+
if (!controller.prepareReset) throw new BadRequest("Reset redemption unavailable");
|
|
1728
|
+
return ok(await controller.prepareReset(readString(payload, "account"), signal));
|
|
1729
|
+
case "consumeReset":
|
|
1730
|
+
if (!controller.consumeReset) throw new BadRequest("Reset redemption unavailable");
|
|
1731
|
+
await controller.consumeReset(readString(payload, "account"), readString(payload, "ticket"), signal);
|
|
1732
|
+
return ok({ ok: true });
|
|
1622
1733
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
1623
1734
|
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
1624
1735
|
case "speed": return ok(await speed.speed(readSessionId(payload)));
|
|
@@ -1689,10 +1800,10 @@ function tokenExpiry(token, now) {
|
|
|
1689
1800
|
}
|
|
1690
1801
|
function abortableDelay$2(ms, signal) {
|
|
1691
1802
|
if (signal.aborted) return Promise.reject(signal.reason);
|
|
1692
|
-
return new Promise((resolve$
|
|
1803
|
+
return new Promise((resolve$2, reject) => {
|
|
1693
1804
|
const timer = setTimeout(() => {
|
|
1694
1805
|
signal.removeEventListener("abort", aborted);
|
|
1695
|
-
resolve$
|
|
1806
|
+
resolve$2();
|
|
1696
1807
|
}, ms);
|
|
1697
1808
|
function aborted() {
|
|
1698
1809
|
clearTimeout(timer);
|
|
@@ -2180,7 +2291,7 @@ async function nativeFetch(url, init = {}) {
|
|
|
2180
2291
|
const client = target.protocol === "https:" ? https : http;
|
|
2181
2292
|
const method = (init.method ?? "GET").toUpperCase();
|
|
2182
2293
|
const body = init.body === void 0 ? void 0 : isBytes(init.body) ? Buffer.from(init.body) : String(init.body);
|
|
2183
|
-
const request = (u$1, redirect) => new Promise((resolve$
|
|
2294
|
+
const request = (u$1, redirect) => new Promise((resolve$2, reject) => {
|
|
2184
2295
|
const req = client.request(u$1, {
|
|
2185
2296
|
method,
|
|
2186
2297
|
headers: sanitizeHeaders(init.headers),
|
|
@@ -2197,10 +2308,10 @@ async function nativeFetch(url, init = {}) {
|
|
|
2197
2308
|
}
|
|
2198
2309
|
const location = res.headers.location;
|
|
2199
2310
|
if ((redirect ?? "follow") === "follow" && location && status >= 300 && status < 400) {
|
|
2200
|
-
resolve$
|
|
2311
|
+
resolve$2({ redirect: new URL(location, u$1) });
|
|
2201
2312
|
return;
|
|
2202
2313
|
}
|
|
2203
|
-
resolve$
|
|
2314
|
+
resolve$2({ response: makeResponse(status, res, data) });
|
|
2204
2315
|
});
|
|
2205
2316
|
});
|
|
2206
2317
|
req.on("error", reject);
|
|
@@ -2358,12 +2469,12 @@ function isSuccessfulAgentResponse(status, contentType) {
|
|
|
2358
2469
|
function abortableDelay$1(ms, signal) {
|
|
2359
2470
|
if (signal.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Cursor retry aborted"));
|
|
2360
2471
|
if (ms === 0) return Promise.resolve();
|
|
2361
|
-
return new Promise((resolve$
|
|
2472
|
+
return new Promise((resolve$2, reject) => {
|
|
2362
2473
|
const timer = setTimeout(done, ms);
|
|
2363
2474
|
timer.unref?.();
|
|
2364
2475
|
function done() {
|
|
2365
2476
|
signal.removeEventListener("abort", aborted);
|
|
2366
|
-
resolve$
|
|
2477
|
+
resolve$2();
|
|
2367
2478
|
}
|
|
2368
2479
|
function aborted() {
|
|
2369
2480
|
clearTimeout(timer);
|
|
@@ -3143,8 +3254,8 @@ var ConnectFrameReader = class {
|
|
|
3143
3254
|
if (this.error !== void 0) throw this.error;
|
|
3144
3255
|
return;
|
|
3145
3256
|
}
|
|
3146
|
-
return new Promise((resolve$
|
|
3147
|
-
resolve: resolve$
|
|
3257
|
+
return new Promise((resolve$2, reject) => this.waiters.push({
|
|
3258
|
+
resolve: resolve$2,
|
|
3148
3259
|
reject
|
|
3149
3260
|
}));
|
|
3150
3261
|
}
|
|
@@ -3182,8 +3293,8 @@ var AgentRun = class {
|
|
|
3182
3293
|
authorization: `Bearer ${this.accessToken}`,
|
|
3183
3294
|
...AGENT_HEADERS
|
|
3184
3295
|
};
|
|
3185
|
-
this.responsePromise = new Promise((resolve$
|
|
3186
|
-
this.resolveResponse = resolve$
|
|
3296
|
+
this.responsePromise = new Promise((resolve$2, reject) => {
|
|
3297
|
+
this.resolveResponse = resolve$2;
|
|
3187
3298
|
this.rejectResponse = reject;
|
|
3188
3299
|
});
|
|
3189
3300
|
this.responsePromise.catch(() => {});
|
|
@@ -3212,10 +3323,10 @@ var AgentRun = class {
|
|
|
3212
3323
|
});
|
|
3213
3324
|
}
|
|
3214
3325
|
#resolveResponse(status) {
|
|
3215
|
-
const resolve$
|
|
3326
|
+
const resolve$2 = this.resolveResponse;
|
|
3216
3327
|
this.resolveResponse = void 0;
|
|
3217
3328
|
this.rejectResponse = void 0;
|
|
3218
|
-
resolve$
|
|
3329
|
+
resolve$2?.(status);
|
|
3219
3330
|
}
|
|
3220
3331
|
#rejectResponse(error) {
|
|
3221
3332
|
const reject = this.rejectResponse;
|
|
@@ -3226,10 +3337,10 @@ var AgentRun = class {
|
|
|
3226
3337
|
waitForResponse(timeoutMs) {
|
|
3227
3338
|
if (this.responsePromise === void 0) throw new Error("Cursor AgentRun has not started");
|
|
3228
3339
|
if (timeoutMs === void 0) return this.responsePromise;
|
|
3229
|
-
return new Promise((resolve$
|
|
3340
|
+
return new Promise((resolve$2, reject) => {
|
|
3230
3341
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Cursor HTTP response timeout")), timeoutMs);
|
|
3231
3342
|
timer.unref?.();
|
|
3232
|
-
this.responsePromise.then(resolve$
|
|
3343
|
+
this.responsePromise.then(resolve$2, reject).finally(() => clearTimeout(timer));
|
|
3233
3344
|
});
|
|
3234
3345
|
}
|
|
3235
3346
|
write(payload) {
|
|
@@ -4771,31 +4882,6 @@ function earliestReset(...candidates) {
|
|
|
4771
4882
|
function waitFromReset(instant, now) {
|
|
4772
4883
|
return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
|
|
4773
4884
|
}
|
|
4774
|
-
/** Header names worth showing when a 429 disclosed no reset this code recognizes. */
|
|
4775
|
-
const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
|
|
4776
|
-
/**
|
|
4777
|
-
* Render the rate-limit-shaped headers and the head of the body of a 429 whose
|
|
4778
|
-
* reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
|
|
4779
|
-
* how an unrecognized provider field gets named from live traffic instead of
|
|
4780
|
-
* being guessed at.
|
|
4781
|
-
*
|
|
4782
|
-
* It is also where the per-bucket rollover snapshots land by design — no reader
|
|
4783
|
-
* parks a turn on one, because on a 429 they cannot say which bucket refused —
|
|
4784
|
-
* so the operator still sees what the provider disclosed.
|
|
4785
|
-
* @param response - the failed response.
|
|
4786
|
-
* @param body - the complete response body.
|
|
4787
|
-
* @returns a one-line diagnostic.
|
|
4788
|
-
*/
|
|
4789
|
-
function rateLimitDiagnostics(response, body) {
|
|
4790
|
-
const headers = [];
|
|
4791
|
-
response.headers.forEach((value, key) => {
|
|
4792
|
-
if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
|
|
4793
|
-
});
|
|
4794
|
-
headers.sort();
|
|
4795
|
-
const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
|
|
4796
|
-
const head = body.slice(0, 200);
|
|
4797
|
-
return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
|
|
4798
|
-
}
|
|
4799
4885
|
/**
|
|
4800
4886
|
* The retry shape every subscription route starts from: Claude Code's own SDK
|
|
4801
4887
|
* numbers — ten retries after the first attempt, exponential backoff from 1s
|
|
@@ -4896,6 +4982,17 @@ function validateModels(models, label) {
|
|
|
4896
4982
|
});
|
|
4897
4983
|
}
|
|
4898
4984
|
/**
|
|
4985
|
+
* Provider failures can echo credentials or customer input in any format.
|
|
4986
|
+
* Shape-based filtering cannot make arbitrary response text safe to display.
|
|
4987
|
+
* Here, safe means this diagnostic carries no provider-controlled free text,
|
|
4988
|
+
* not that secrets cannot exist elsewhere in the system. Keep raw responses
|
|
4989
|
+
* internal for classification and reset parsing; display only local summaries.
|
|
4990
|
+
* This deliberately loses model, parameter and plan details useful to support.
|
|
4991
|
+
*/
|
|
4992
|
+
function providerDiagnostic(summary, hasBody) {
|
|
4993
|
+
return hasBody ? `${summary}: [provider response body omitted]` : summary;
|
|
4994
|
+
}
|
|
4995
|
+
/**
|
|
4899
4996
|
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
4900
4997
|
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
4901
4998
|
* the `providerRetryAfterMs` the retry plugin waits out.
|
|
@@ -4916,7 +5013,6 @@ async function httpLlmError(response, label, options = {}) {
|
|
|
4916
5013
|
body = await response.text();
|
|
4917
5014
|
} catch {}
|
|
4918
5015
|
const shown = body.slice(0, 500);
|
|
4919
|
-
const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
|
|
4920
5016
|
let code;
|
|
4921
5017
|
if (response.status === 401 || response.status === 403) code = "AUTH";
|
|
4922
5018
|
else if (response.status === 429) code = "RATE_LIMIT";
|
|
@@ -4925,10 +5021,11 @@ async function httpLlmError(response, label, options = {}) {
|
|
|
4925
5021
|
else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
|
|
4926
5022
|
else if (response.status >= 500) code = "SERVER";
|
|
4927
5023
|
else code = `HTTP_${String(response.status)}`;
|
|
5024
|
+
const message = providerDiagnostic(`${label} error (HTTP ${String(response.status)}, ${code})`, body.length > 0);
|
|
4928
5025
|
const now = Date.now();
|
|
4929
5026
|
const rateLimited = response.status === 429;
|
|
4930
5027
|
const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? googleQuotaReset(body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
|
|
4931
|
-
if (reset === void 0 && rateLimited) options.onWarn?.(`${label}:
|
|
5028
|
+
if (reset === void 0 && rateLimited) options.onWarn?.(providerDiagnostic(`${label}: 429 disclosed no reset time; [provider response headers omitted]`, body.length > 0));
|
|
4932
5029
|
return new LlmError(message, code, {
|
|
4933
5030
|
status: response.status,
|
|
4934
5031
|
...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
|
|
@@ -5052,19 +5149,17 @@ var OAuthEndpointError = class extends Error {
|
|
|
5052
5149
|
*/
|
|
5053
5150
|
async function oauthEndpointError(response, label) {
|
|
5054
5151
|
let oauthCode;
|
|
5055
|
-
let
|
|
5152
|
+
let body = "";
|
|
5056
5153
|
try {
|
|
5057
|
-
|
|
5154
|
+
body = await response.text();
|
|
5155
|
+
const parsed = JSON.parse(body);
|
|
5058
5156
|
if (typeof parsed.error === "string") oauthCode = parsed.error;
|
|
5059
5157
|
else if (typeof parsed.error === "object" && parsed.error !== null) {
|
|
5060
5158
|
const nested = parsed.error;
|
|
5061
5159
|
if (typeof nested.code === "string" && nested.code.length > 0) oauthCode = nested.code;
|
|
5062
|
-
if (typeof nested.message === "string") detail = nested.message;
|
|
5063
5160
|
}
|
|
5064
|
-
if (typeof parsed.error_description === "string") detail = parsed.error_description;
|
|
5065
|
-
if (detail.length === 0) detail = oauthCode ?? "";
|
|
5066
5161
|
} catch {}
|
|
5067
|
-
return new OAuthEndpointError(
|
|
5162
|
+
return new OAuthEndpointError(providerDiagnostic(`${label} token endpoint error (HTTP ${String(response.status)})`, body.length > 0), response.status, oauthCode, parseRetryAfterMs(response));
|
|
5068
5163
|
}
|
|
5069
5164
|
/**
|
|
5070
5165
|
* Per-provider session freshness: loads the stored session, refreshes
|
|
@@ -5143,9 +5238,9 @@ const DISCOVERY_TIMEOUT_MS = 1e4;
|
|
|
5143
5238
|
*/
|
|
5144
5239
|
function withTimeout(work, timeoutMs) {
|
|
5145
5240
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
5146
|
-
const aborted = new Promise((resolve$
|
|
5147
|
-
if (signal.aborted) resolve$
|
|
5148
|
-
else signal.addEventListener("abort", () => resolve$
|
|
5241
|
+
const aborted = new Promise((resolve$2) => {
|
|
5242
|
+
if (signal.aborted) resolve$2(void 0);
|
|
5243
|
+
else signal.addEventListener("abort", () => resolve$2(void 0), { once: true });
|
|
5149
5244
|
});
|
|
5150
5245
|
return Promise.race([work(signal).then((value) => signal.aborted ? void 0 : value, (error) => {
|
|
5151
5246
|
if (signal.aborted) return void 0;
|
|
@@ -5504,429 +5599,127 @@ var AccountTokenManager = class {
|
|
|
5504
5599
|
};
|
|
5505
5600
|
|
|
5506
5601
|
//#endregion
|
|
5507
|
-
//#region src/
|
|
5602
|
+
//#region src/compat.ts
|
|
5603
|
+
/** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
|
|
5604
|
+
const ToolCallId$1 = (() => {
|
|
5605
|
+
const exports = llm;
|
|
5606
|
+
return exports["ToolCallId"] ?? exports["CallId"];
|
|
5607
|
+
})();
|
|
5608
|
+
|
|
5609
|
+
//#endregion
|
|
5610
|
+
//#region src/translate/sse.ts
|
|
5508
5611
|
/**
|
|
5509
|
-
*
|
|
5510
|
-
* @
|
|
5612
|
+
* Decode an SSE byte stream into events.
|
|
5613
|
+
* @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
|
|
5614
|
+
* @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
|
|
5615
|
+
* @returns events in arrival order.
|
|
5511
5616
|
*/
|
|
5512
|
-
function
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5617
|
+
async function* parseSse(stream, onActivity) {
|
|
5618
|
+
const reader = stream.getReader();
|
|
5619
|
+
const decoder = new TextDecoder();
|
|
5620
|
+
let pending = "";
|
|
5621
|
+
let dataLines = [];
|
|
5622
|
+
let eventName;
|
|
5623
|
+
try {
|
|
5624
|
+
while (true) {
|
|
5625
|
+
const { done, value } = await reader.read();
|
|
5626
|
+
if (done) return;
|
|
5627
|
+
onActivity?.();
|
|
5628
|
+
pending += decoder.decode(value, { stream: true });
|
|
5629
|
+
let newline = pending.indexOf("\n");
|
|
5630
|
+
while (newline >= 0) {
|
|
5631
|
+
let line = pending.slice(0, newline);
|
|
5632
|
+
pending = pending.slice(newline + 1);
|
|
5633
|
+
newline = pending.indexOf("\n");
|
|
5634
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
5635
|
+
if (line.length === 0) {
|
|
5636
|
+
if (dataLines.length > 0) yield {
|
|
5637
|
+
data: dataLines.join("\n"),
|
|
5638
|
+
...eventName === void 0 ? {} : { event: eventName }
|
|
5639
|
+
};
|
|
5640
|
+
dataLines = [];
|
|
5641
|
+
eventName = void 0;
|
|
5642
|
+
} else if (line.startsWith(":")) onActivity?.();
|
|
5643
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
5644
|
+
else if (line.startsWith("event:")) eventName = line.slice(6).replace(/^ /, "");
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
} finally {
|
|
5648
|
+
try {
|
|
5649
|
+
await reader.cancel();
|
|
5650
|
+
} catch {}
|
|
5651
|
+
reader.releaseLock();
|
|
5540
5652
|
}
|
|
5541
|
-
if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
|
|
5542
|
-
return {
|
|
5543
|
-
efforts,
|
|
5544
|
-
...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
|
|
5545
|
-
};
|
|
5546
|
-
}
|
|
5547
|
-
/** Validate one persisted model, or undefined when malformed. */
|
|
5548
|
-
function sanitizeModel(value) {
|
|
5549
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
5550
|
-
const raw = value;
|
|
5551
|
-
if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.maxContextWindow !== void 0 && (typeof raw.maxContextWindow !== "number" || !Number.isSafeInteger(raw.maxContextWindow) || raw.maxContextWindow <= 0) || raw.maxOutputTokens !== void 0 && (typeof raw.maxOutputTokens !== "number" || !Number.isSafeInteger(raw.maxOutputTokens) || raw.maxOutputTokens <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
|
|
5552
|
-
const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
|
|
5553
|
-
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
5554
|
-
const thinkingType = raw.thinkingType;
|
|
5555
|
-
if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
|
|
5556
|
-
const fastTier = raw.fastTier;
|
|
5557
|
-
if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
|
|
5558
|
-
const copilotWire = raw.copilotWire;
|
|
5559
|
-
if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
|
|
5560
|
-
const copilotResponses = raw.copilotResponses;
|
|
5561
|
-
if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
|
|
5562
|
-
const inputModalities = raw.inputModalities;
|
|
5563
|
-
if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
|
|
5564
|
-
return {
|
|
5565
|
-
id: raw.id,
|
|
5566
|
-
name: raw.name,
|
|
5567
|
-
...raw.description === void 0 ? {} : { description: raw.description },
|
|
5568
|
-
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
5569
|
-
...raw.maxContextWindow === void 0 ? {} : { maxContextWindow: raw.maxContextWindow },
|
|
5570
|
-
...raw.maxOutputTokens === void 0 ? {} : { maxOutputTokens: raw.maxOutputTokens },
|
|
5571
|
-
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
5572
|
-
...reasoning === void 0 ? {} : { reasoning },
|
|
5573
|
-
...thinkingType === void 0 ? {} : { thinkingType },
|
|
5574
|
-
...fastTier === void 0 ? {} : { fastTier },
|
|
5575
|
-
...copilotWire === void 0 ? {} : { copilotWire },
|
|
5576
|
-
...copilotResponses === void 0 ? {} : { copilotResponses },
|
|
5577
|
-
...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
|
|
5578
|
-
};
|
|
5579
5653
|
}
|
|
5654
|
+
|
|
5655
|
+
//#endregion
|
|
5656
|
+
//#region src/translate/anthropic.ts
|
|
5580
5657
|
/**
|
|
5581
|
-
*
|
|
5582
|
-
*
|
|
5583
|
-
*
|
|
5584
|
-
* @param value - the raw per-provider file entry.
|
|
5585
|
-
* @returns the validated snapshot, or undefined when unusable.
|
|
5658
|
+
* The Claude Code identity block. The subscription endpoint rejects requests
|
|
5659
|
+
* that do not present as Claude Code, so this block is REQUIRED as the first
|
|
5660
|
+
* system entry on every request.
|
|
5586
5661
|
*/
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5662
|
+
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
5663
|
+
/** Tags wrapping a mid-conversation system message where it sits in the history. */
|
|
5664
|
+
const SYSTEM_REMINDER_OPEN = "<system-reminder>";
|
|
5665
|
+
const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
|
|
5666
|
+
/**
|
|
5667
|
+
* How far apart consecutive message breakpoints sit, in content blocks.
|
|
5668
|
+
*
|
|
5669
|
+
* A breakpoint looks back at most 20 blocks for an entry an earlier request
|
|
5670
|
+
* wrote, so marks must stay closer than that: one agentic turn can append a
|
|
5671
|
+
* dozen tool_use/tool_result blocks at once, and a single trailing mark would
|
|
5672
|
+
* silently fall out of range and rebuild the whole prefix.
|
|
5673
|
+
*/
|
|
5674
|
+
const CACHE_BLOCK_STRIDE = 15;
|
|
5675
|
+
/**
|
|
5676
|
+
* Message breakpoints per request. Anthropic allows four in total and the
|
|
5677
|
+
* last `system` block takes the fourth, so three are left for the history —
|
|
5678
|
+
* enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
|
|
5679
|
+
* blocks before a read is lost.
|
|
5680
|
+
*/
|
|
5681
|
+
const MESSAGE_CACHE_BREAKPOINTS = 3;
|
|
5682
|
+
/** The API default; a marker at this TTL carries no `ttl` field at all. */
|
|
5683
|
+
const DEFAULT_PROMPT_CACHE_TTL = "5m";
|
|
5684
|
+
/**
|
|
5685
|
+
* Build one `cache_control` marker.
|
|
5686
|
+
*
|
|
5687
|
+
* Five minutes is spelled by omitting `ttl`, so a request that does not opt in
|
|
5688
|
+
* stays byte-identical to what earlier releases sent.
|
|
5689
|
+
* @param ttl - cache lifetime; defaults to five minutes.
|
|
5690
|
+
* @returns a fresh marker object (callers attach it to one block each).
|
|
5691
|
+
*/
|
|
5692
|
+
function cacheControl(ttl = DEFAULT_PROMPT_CACHE_TTL) {
|
|
5693
|
+
return ttl === "1h" ? {
|
|
5694
|
+
type: "ephemeral",
|
|
5695
|
+
ttl: "1h"
|
|
5696
|
+
} : { type: "ephemeral" };
|
|
5697
|
+
}
|
|
5698
|
+
/** Preserve native image blocks, retaining the existing text-only wire shape. */
|
|
5699
|
+
function toolResultContent(block) {
|
|
5700
|
+
if (!block.content.some((part) => part.type === "image" && "dataBase64" in part)) return block.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
5701
|
+
const content = [];
|
|
5702
|
+
for (const part of block.content) {
|
|
5703
|
+
if (part.type === "text" && part.text.length > 0) content.push({
|
|
5704
|
+
type: "text",
|
|
5705
|
+
text: part.text
|
|
5706
|
+
});
|
|
5707
|
+
if (part.type === "image" && "dataBase64" in part) content.push({
|
|
5708
|
+
type: "image",
|
|
5709
|
+
source: {
|
|
5710
|
+
type: "base64",
|
|
5711
|
+
media_type: part.mediaType,
|
|
5712
|
+
data: part.dataBase64
|
|
5713
|
+
}
|
|
5714
|
+
});
|
|
5599
5715
|
}
|
|
5600
|
-
return
|
|
5601
|
-
at: raw.at,
|
|
5602
|
-
models
|
|
5603
|
-
};
|
|
5716
|
+
return content;
|
|
5604
5717
|
}
|
|
5605
|
-
/**
|
|
5606
|
-
|
|
5607
|
-
let text;
|
|
5718
|
+
/** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
|
|
5719
|
+
function parseToolInput(raw) {
|
|
5608
5720
|
try {
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
return {};
|
|
5612
|
-
}
|
|
5613
|
-
try {
|
|
5614
|
-
const parsed = JSON.parse(text);
|
|
5615
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
5616
|
-
return parsed;
|
|
5617
|
-
} catch {
|
|
5618
|
-
return {};
|
|
5619
|
-
}
|
|
5620
|
-
}
|
|
5621
|
-
/** Persist the whole file atomically (tmp file + rename). */
|
|
5622
|
-
async function writeCatalogFile(store, path) {
|
|
5623
|
-
await mkdir(dirname(path), { recursive: true });
|
|
5624
|
-
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
5625
|
-
try {
|
|
5626
|
-
await writeFile(tmp, JSON.stringify(store, null, 2));
|
|
5627
|
-
await rename(tmp, path);
|
|
5628
|
-
} catch (error) {
|
|
5629
|
-
await rm(tmp, { force: true });
|
|
5630
|
-
throw error;
|
|
5631
|
-
}
|
|
5632
|
-
}
|
|
5633
|
-
/**
|
|
5634
|
-
* Build the durable half of one provider's catalog cache over the shared
|
|
5635
|
-
* models.json file (concurrent writers are last-writer-wins, acceptable for
|
|
5636
|
-
* a cache).
|
|
5637
|
-
* @param provider - the provider route keying the file entry.
|
|
5638
|
-
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
5639
|
-
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
5640
|
-
*/
|
|
5641
|
-
function catalogStore(provider, path = modelsFilePath()) {
|
|
5642
|
-
return {
|
|
5643
|
-
async load() {
|
|
5644
|
-
return sanitizeSnapshot((await readCatalogFile(path))[provider]);
|
|
5645
|
-
},
|
|
5646
|
-
async save(snapshot) {
|
|
5647
|
-
const store = await readCatalogFile(path);
|
|
5648
|
-
store[provider] = snapshot;
|
|
5649
|
-
await writeCatalogFile(store, path);
|
|
5650
|
-
},
|
|
5651
|
-
async clear() {
|
|
5652
|
-
const store = await readCatalogFile(path);
|
|
5653
|
-
if (store[provider] === void 0) return;
|
|
5654
|
-
delete store[provider];
|
|
5655
|
-
await writeCatalogFile(store, path);
|
|
5656
|
-
}
|
|
5657
|
-
};
|
|
5658
|
-
}
|
|
5659
|
-
/**
|
|
5660
|
-
* The durable half of one NON-default account's catalog cache. The provider
|
|
5661
|
-
* entry belongs to the default account; every other account keeps its own
|
|
5662
|
-
* snapshot under `accounts[provider][account]`, so a working secondary login
|
|
5663
|
-
* survives restarts and network failures even when the default login is dead.
|
|
5664
|
-
* @param provider - the provider route.
|
|
5665
|
-
* @param account - the canonical account key.
|
|
5666
|
-
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
5667
|
-
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
5668
|
-
*/
|
|
5669
|
-
function accountCatalogStore(provider, account, path = modelsFilePath()) {
|
|
5670
|
-
return {
|
|
5671
|
-
async load() {
|
|
5672
|
-
const section = accountSection(await readCatalogFile(path), provider);
|
|
5673
|
-
return section !== void 0 && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : void 0;
|
|
5674
|
-
},
|
|
5675
|
-
async save(snapshot) {
|
|
5676
|
-
const store = await readCatalogFile(path);
|
|
5677
|
-
const accounts = typeof store.accounts === "object" && store.accounts !== null && !Array.isArray(store.accounts) ? store.accounts : {};
|
|
5678
|
-
const section = {
|
|
5679
|
-
...accountSection(store, provider) ?? {},
|
|
5680
|
-
[account]: snapshot
|
|
5681
|
-
};
|
|
5682
|
-
store.accounts = {
|
|
5683
|
-
...accounts,
|
|
5684
|
-
[provider]: section
|
|
5685
|
-
};
|
|
5686
|
-
await writeCatalogFile(store, path);
|
|
5687
|
-
},
|
|
5688
|
-
async clear() {
|
|
5689
|
-
const store = await readCatalogFile(path);
|
|
5690
|
-
const section = accountSection(store, provider);
|
|
5691
|
-
if (section === void 0 || !Object.hasOwn(section, account)) return;
|
|
5692
|
-
const rest = { ...section };
|
|
5693
|
-
delete rest[account];
|
|
5694
|
-
store.accounts = {
|
|
5695
|
-
...store.accounts,
|
|
5696
|
-
[provider]: rest
|
|
5697
|
-
};
|
|
5698
|
-
await writeCatalogFile(store, path);
|
|
5699
|
-
}
|
|
5700
|
-
};
|
|
5701
|
-
}
|
|
5702
|
-
|
|
5703
|
-
//#endregion
|
|
5704
|
-
//#region src/translate/resolved.ts
|
|
5705
|
-
/**
|
|
5706
|
-
* Wires with text-only tool outputs receive images in a following user turn.
|
|
5707
|
-
* Defer that turn until all consecutive user messages have been processed:
|
|
5708
|
-
* parallel tool results can arrive in separate harness messages, and a user
|
|
5709
|
-
* image message must not interrupt their tool-call/output pairing.
|
|
5710
|
-
*/
|
|
5711
|
-
function withToolResultImages(messages) {
|
|
5712
|
-
const out = [];
|
|
5713
|
-
let images = [];
|
|
5714
|
-
const flush = () => {
|
|
5715
|
-
if (images.length > 0) out.push({
|
|
5716
|
-
role: "user",
|
|
5717
|
-
content: images
|
|
5718
|
-
});
|
|
5719
|
-
images = [];
|
|
5720
|
-
};
|
|
5721
|
-
for (const message of messages) {
|
|
5722
|
-
if (message.role === "assistant") flush();
|
|
5723
|
-
out.push(message);
|
|
5724
|
-
if (message.role === "tool") {
|
|
5725
|
-
const parts = message.content.filter((part) => part.type === "image" && "dataBase64" in part);
|
|
5726
|
-
if (parts.length > 0) images.push({
|
|
5727
|
-
type: "text",
|
|
5728
|
-
text: `Images from tool result ${String(message.toolCallId)}:`
|
|
5729
|
-
}, ...parts);
|
|
5730
|
-
}
|
|
5731
|
-
for (const block of message.content) {
|
|
5732
|
-
if (block.type !== "tool-result") continue;
|
|
5733
|
-
const parts = block.content.filter((part) => part.type === "image" && "dataBase64" in part);
|
|
5734
|
-
if (parts.length > 0) images.push({
|
|
5735
|
-
type: "text",
|
|
5736
|
-
text: `Images from tool result ${String(block.toolCallId)}:`
|
|
5737
|
-
}, ...parts);
|
|
5738
|
-
}
|
|
5739
|
-
}
|
|
5740
|
-
flush();
|
|
5741
|
-
return out;
|
|
5742
|
-
}
|
|
5743
|
-
/**
|
|
5744
|
-
* The request projection for one oversized image, or undefined when it fits.
|
|
5745
|
-
* Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
|
|
5746
|
-
* target edges (`width`/`height`); each validates only its own fields, so
|
|
5747
|
-
* one projection carries both.
|
|
5748
|
-
*/
|
|
5749
|
-
function imageRequestTarget(ref, limit) {
|
|
5750
|
-
const longEdge = Math.max(ref.width, ref.height);
|
|
5751
|
-
if (!(longEdge > limit.maxEdge)) return void 0;
|
|
5752
|
-
const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
|
|
5753
|
-
const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
|
|
5754
|
-
const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
|
|
5755
|
-
return {
|
|
5756
|
-
width,
|
|
5757
|
-
height,
|
|
5758
|
-
maxPixels: width * height,
|
|
5759
|
-
maxBytes: limit.maxBytes
|
|
5760
|
-
};
|
|
5761
|
-
}
|
|
5762
|
-
/**
|
|
5763
|
-
* Resolve every ImageBlock's attachment reference to inline base64 bytes.
|
|
5764
|
-
* Messages without images pass through unchanged. A request carrying an image
|
|
5765
|
-
* with no attachment service available fails loudly rather than silently
|
|
5766
|
-
* dropping the image.
|
|
5767
|
-
* @param messages - the request's conversation messages.
|
|
5768
|
-
* @param attachments - the deployment's attachment service, when mounted.
|
|
5769
|
-
* @param signal - cancellation for the storage reads.
|
|
5770
|
-
* @param limit - the route's outgoing image cap; oversized images are sent
|
|
5771
|
-
* downscaled while the stored attachment and history stay untouched.
|
|
5772
|
-
* @returns the same messages with image blocks resolved for the translators.
|
|
5773
|
-
*/
|
|
5774
|
-
async function resolveImages(messages, attachments, signal, limit) {
|
|
5775
|
-
if (messages.some((message) => message.content.some((block) => block.type === "image")) && attachments === void 0) throw new LlmError("dsh-subscriptions: the request carries an image but no attachments service is mounted; image input requires the harness attachment store", "UNSUPPORTED");
|
|
5776
|
-
const readForRequest = async (ref) => {
|
|
5777
|
-
if (attachments === void 0) throw new Error("attachments service is unavailable");
|
|
5778
|
-
const target = limit === void 0 ? void 0 : imageRequestTarget(ref, limit);
|
|
5779
|
-
if (target !== void 0) try {
|
|
5780
|
-
const version = await attachments.readImageRequest(ref, target, signal);
|
|
5781
|
-
return {
|
|
5782
|
-
data: version.data,
|
|
5783
|
-
mediaType: version.mediaType,
|
|
5784
|
-
ref: version.attachment
|
|
5785
|
-
};
|
|
5786
|
-
} catch (error) {
|
|
5787
|
-
if (signal?.aborted) throw error;
|
|
5788
|
-
}
|
|
5789
|
-
const stored = await attachments.readImage(ref, signal);
|
|
5790
|
-
return {
|
|
5791
|
-
data: stored.data,
|
|
5792
|
-
mediaType: stored.ref.mediaType,
|
|
5793
|
-
ref: stored.ref
|
|
5794
|
-
};
|
|
5795
|
-
};
|
|
5796
|
-
const resolveBlock = async (block) => {
|
|
5797
|
-
if (block.type !== "image") return [block];
|
|
5798
|
-
const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
|
|
5799
|
-
const { attachmentId, mediaType, bytes, width, height, name: name$1 } = ref;
|
|
5800
|
-
return [{
|
|
5801
|
-
type: "image",
|
|
5802
|
-
mediaType: sentType,
|
|
5803
|
-
dataBase64: Buffer.from(data).toString("base64")
|
|
5804
|
-
}, {
|
|
5805
|
-
type: "text",
|
|
5806
|
-
text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
|
|
5807
|
-
attachmentId,
|
|
5808
|
-
mediaType,
|
|
5809
|
-
bytes,
|
|
5810
|
-
width,
|
|
5811
|
-
height,
|
|
5812
|
-
...name$1 === void 0 ? {} : { name: name$1 }
|
|
5813
|
-
})}`
|
|
5814
|
-
}];
|
|
5815
|
-
};
|
|
5816
|
-
return Promise.all(messages.map(async (message) => ({
|
|
5817
|
-
role: message.role,
|
|
5818
|
-
...message.source === void 0 ? {} : { source: message.source },
|
|
5819
|
-
...message.role === "tool" ? {
|
|
5820
|
-
toolCallId: message.toolCallId,
|
|
5821
|
-
isError: message.isError
|
|
5822
|
-
} : {},
|
|
5823
|
-
content: (await Promise.all(message.content.map(resolveBlock))).flat()
|
|
5824
|
-
})));
|
|
5825
|
-
}
|
|
5826
|
-
|
|
5827
|
-
//#endregion
|
|
5828
|
-
//#region src/compat.ts
|
|
5829
|
-
/** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
|
|
5830
|
-
const ToolCallId$1 = (() => {
|
|
5831
|
-
const exports = llm;
|
|
5832
|
-
return exports["ToolCallId"] ?? exports["CallId"];
|
|
5833
|
-
})();
|
|
5834
|
-
|
|
5835
|
-
//#endregion
|
|
5836
|
-
//#region src/translate/sse.ts
|
|
5837
|
-
/**
|
|
5838
|
-
* Decode an SSE byte stream into events.
|
|
5839
|
-
* @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
|
|
5840
|
-
* @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
|
|
5841
|
-
* @returns events in arrival order.
|
|
5842
|
-
*/
|
|
5843
|
-
async function* parseSse(stream, onActivity) {
|
|
5844
|
-
const reader = stream.getReader();
|
|
5845
|
-
const decoder = new TextDecoder();
|
|
5846
|
-
let pending = "";
|
|
5847
|
-
let dataLines = [];
|
|
5848
|
-
let eventName;
|
|
5849
|
-
try {
|
|
5850
|
-
while (true) {
|
|
5851
|
-
const { done, value } = await reader.read();
|
|
5852
|
-
if (done) return;
|
|
5853
|
-
onActivity?.();
|
|
5854
|
-
pending += decoder.decode(value, { stream: true });
|
|
5855
|
-
let newline = pending.indexOf("\n");
|
|
5856
|
-
while (newline >= 0) {
|
|
5857
|
-
let line = pending.slice(0, newline);
|
|
5858
|
-
pending = pending.slice(newline + 1);
|
|
5859
|
-
newline = pending.indexOf("\n");
|
|
5860
|
-
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
5861
|
-
if (line.length === 0) {
|
|
5862
|
-
if (dataLines.length > 0) yield {
|
|
5863
|
-
data: dataLines.join("\n"),
|
|
5864
|
-
...eventName === void 0 ? {} : { event: eventName }
|
|
5865
|
-
};
|
|
5866
|
-
dataLines = [];
|
|
5867
|
-
eventName = void 0;
|
|
5868
|
-
} else if (line.startsWith(":")) onActivity?.();
|
|
5869
|
-
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
5870
|
-
else if (line.startsWith("event:")) eventName = line.slice(6).replace(/^ /, "");
|
|
5871
|
-
}
|
|
5872
|
-
}
|
|
5873
|
-
} finally {
|
|
5874
|
-
reader.releaseLock();
|
|
5875
|
-
}
|
|
5876
|
-
}
|
|
5877
|
-
|
|
5878
|
-
//#endregion
|
|
5879
|
-
//#region src/translate/anthropic.ts
|
|
5880
|
-
/**
|
|
5881
|
-
* The Claude Code identity block. The subscription endpoint rejects requests
|
|
5882
|
-
* that do not present as Claude Code, so this block is REQUIRED as the first
|
|
5883
|
-
* system entry on every request.
|
|
5884
|
-
*/
|
|
5885
|
-
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
5886
|
-
/** Tags wrapping a mid-conversation system message where it sits in the history. */
|
|
5887
|
-
const SYSTEM_REMINDER_OPEN = "<system-reminder>";
|
|
5888
|
-
const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
|
|
5889
|
-
/**
|
|
5890
|
-
* How far apart consecutive message breakpoints sit, in content blocks.
|
|
5891
|
-
*
|
|
5892
|
-
* A breakpoint looks back at most 20 blocks for an entry an earlier request
|
|
5893
|
-
* wrote, so marks must stay closer than that: one agentic turn can append a
|
|
5894
|
-
* dozen tool_use/tool_result blocks at once, and a single trailing mark would
|
|
5895
|
-
* silently fall out of range and rebuild the whole prefix.
|
|
5896
|
-
*/
|
|
5897
|
-
const CACHE_BLOCK_STRIDE = 15;
|
|
5898
|
-
/**
|
|
5899
|
-
* Message breakpoints per request. Anthropic allows four in total and the
|
|
5900
|
-
* last `system` block takes the fourth, so three are left for the history —
|
|
5901
|
-
* enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
|
|
5902
|
-
* blocks before a read is lost.
|
|
5903
|
-
*/
|
|
5904
|
-
const MESSAGE_CACHE_BREAKPOINTS = 3;
|
|
5905
|
-
/** Preserve native image blocks, retaining the existing text-only wire shape. */
|
|
5906
|
-
function toolResultContent(block) {
|
|
5907
|
-
if (!block.content.some((part) => part.type === "image" && "dataBase64" in part)) return block.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
5908
|
-
const content = [];
|
|
5909
|
-
for (const part of block.content) {
|
|
5910
|
-
if (part.type === "text" && part.text.length > 0) content.push({
|
|
5911
|
-
type: "text",
|
|
5912
|
-
text: part.text
|
|
5913
|
-
});
|
|
5914
|
-
if (part.type === "image" && "dataBase64" in part) content.push({
|
|
5915
|
-
type: "image",
|
|
5916
|
-
source: {
|
|
5917
|
-
type: "base64",
|
|
5918
|
-
media_type: part.mediaType,
|
|
5919
|
-
data: part.dataBase64
|
|
5920
|
-
}
|
|
5921
|
-
});
|
|
5922
|
-
}
|
|
5923
|
-
return content;
|
|
5924
|
-
}
|
|
5925
|
-
/** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
|
|
5926
|
-
function parseToolInput(raw) {
|
|
5927
|
-
try {
|
|
5928
|
-
const parsed = JSON.parse(raw);
|
|
5929
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
5721
|
+
const parsed = JSON.parse(raw);
|
|
5722
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
5930
5723
|
return {};
|
|
5931
5724
|
} catch {
|
|
5932
5725
|
return {};
|
|
@@ -6068,13 +5861,14 @@ function toAnthropicMessages(messages) {
|
|
|
6068
5861
|
* Marks are counted across the flattened block sequence, not per message,
|
|
6069
5862
|
* because the lookback window Anthropic walks counts blocks the same way.
|
|
6070
5863
|
* @param messages - assembled Anthropic messages, marked in place.
|
|
5864
|
+
* @param ttl - cache lifetime for every mark; defaults to five minutes.
|
|
6071
5865
|
*/
|
|
6072
|
-
function markMessageCache(messages) {
|
|
5866
|
+
function markMessageCache(messages, ttl) {
|
|
6073
5867
|
const blocks = messages.flatMap((message) => message.content);
|
|
6074
5868
|
for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
|
|
6075
5869
|
const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
|
|
6076
5870
|
if (at < 0) return;
|
|
6077
|
-
blocks[at].cache_control =
|
|
5871
|
+
blocks[at].cache_control = cacheControl(ttl);
|
|
6078
5872
|
}
|
|
6079
5873
|
}
|
|
6080
5874
|
/**
|
|
@@ -6083,9 +5877,10 @@ function markMessageCache(messages) {
|
|
|
6083
5877
|
* @param system - explicit system prompt, when set.
|
|
6084
5878
|
* @param messages - conversation messages; the system-role text preceding the
|
|
6085
5879
|
* conversation is appended, and a later one is left to {@link toAnthropicMessages}.
|
|
5880
|
+
* @param ttl - cache lifetime for the tools+system mark; defaults to five minutes.
|
|
6086
5881
|
* @returns the system content blocks.
|
|
6087
5882
|
*/
|
|
6088
|
-
function toAnthropicSystem(system, messages) {
|
|
5883
|
+
function toAnthropicSystem(system, messages, ttl) {
|
|
6089
5884
|
const blocks = [{
|
|
6090
5885
|
type: "text",
|
|
6091
5886
|
text: CLAUDE_CODE_IDENTITY
|
|
@@ -6099,7 +5894,7 @@ function toAnthropicSystem(system, messages) {
|
|
|
6099
5894
|
type: "text",
|
|
6100
5895
|
text: block.text
|
|
6101
5896
|
});
|
|
6102
|
-
blocks[blocks.length - 1].cache_control =
|
|
5897
|
+
blocks[blocks.length - 1].cache_control = cacheControl(ttl);
|
|
6103
5898
|
return blocks;
|
|
6104
5899
|
}
|
|
6105
5900
|
/**
|
|
@@ -6147,10 +5942,11 @@ function closeBlock$2(block) {
|
|
|
6147
5942
|
function anthropicFailure(error) {
|
|
6148
5943
|
const type = error?.type ?? "unknown_error";
|
|
6149
5944
|
const message = error?.message ?? `Anthropic reported ${type}`;
|
|
6150
|
-
|
|
6151
|
-
if (type === "
|
|
6152
|
-
if (type === "
|
|
6153
|
-
|
|
5945
|
+
let classification = "SERVER";
|
|
5946
|
+
if (type === "invalid_request_error" && /prompt is too long/i.test(message)) classification = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
5947
|
+
else if (type === "rate_limit_error") classification = "RATE_LIMIT";
|
|
5948
|
+
else if (type === "authentication_error") classification = "AUTH";
|
|
5949
|
+
return new LlmError(`Anthropic failure (${classification}): [provider response body omitted]`, classification);
|
|
6154
5950
|
}
|
|
6155
5951
|
/**
|
|
6156
5952
|
* Push-model Anthropic SSE translator: feed each parsed event object to
|
|
@@ -6339,27 +6135,426 @@ var AnthropicStreamTranslator = class {
|
|
|
6339
6135
|
case "error": throw anthropicFailure(event.error);
|
|
6340
6136
|
default: return chunks;
|
|
6341
6137
|
}
|
|
6342
|
-
}
|
|
6343
|
-
};
|
|
6138
|
+
}
|
|
6139
|
+
};
|
|
6140
|
+
/**
|
|
6141
|
+
* Consume an Anthropic SSE byte stream and yield harness StreamChunks.
|
|
6142
|
+
* @param stream - raw response body.
|
|
6143
|
+
* @param onActivity - transport-activity callback for the idle watchdog.
|
|
6144
|
+
* @returns the chunk stream; throws when the stream ends before `message_stop`.
|
|
6145
|
+
*/
|
|
6146
|
+
async function* streamAnthropic(stream, onActivity) {
|
|
6147
|
+
const translator = new AnthropicStreamTranslator();
|
|
6148
|
+
for await (const sseEvent of parseSse(stream, onActivity)) {
|
|
6149
|
+
let event;
|
|
6150
|
+
try {
|
|
6151
|
+
event = JSON.parse(sseEvent.data);
|
|
6152
|
+
} catch {
|
|
6153
|
+
throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
6154
|
+
}
|
|
6155
|
+
yield* translator.push(event);
|
|
6156
|
+
if (translator.terminated) return;
|
|
6157
|
+
}
|
|
6158
|
+
throw new LlmError("Anthropic SSE stream ended before message_stop", "STREAM_CLOSED");
|
|
6159
|
+
}
|
|
6160
|
+
|
|
6161
|
+
//#endregion
|
|
6162
|
+
//#region src/providers/catalog-store.ts
|
|
6163
|
+
/**
|
|
6164
|
+
* Absolute path of the catalog store file.
|
|
6165
|
+
* @returns `dshHomePath('plugins', 'subscriptions', 'models.json')`.
|
|
6166
|
+
*/
|
|
6167
|
+
function modelsFilePath() {
|
|
6168
|
+
return dshHomePath("plugins", "subscriptions", "models.json");
|
|
6169
|
+
}
|
|
6170
|
+
/** The per-account section of the file, or undefined when absent or malformed. */
|
|
6171
|
+
function accountSection(store, provider) {
|
|
6172
|
+
const accounts = store.accounts;
|
|
6173
|
+
if (typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) return void 0;
|
|
6174
|
+
const section = accounts[provider];
|
|
6175
|
+
if (typeof section !== "object" || section === null || Array.isArray(section)) return void 0;
|
|
6176
|
+
return section;
|
|
6177
|
+
}
|
|
6178
|
+
/** Validate one persisted reasoning block, or undefined when malformed. */
|
|
6179
|
+
function sanitizeReasoning(value) {
|
|
6180
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
6181
|
+
const raw = value;
|
|
6182
|
+
if (!Array.isArray(raw.efforts) || raw.efforts.length === 0) return void 0;
|
|
6183
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6184
|
+
const efforts = [];
|
|
6185
|
+
for (const entry of raw.efforts) {
|
|
6186
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
6187
|
+
const effort = entry;
|
|
6188
|
+
if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) return void 0;
|
|
6189
|
+
seen.add(effort.id);
|
|
6190
|
+
efforts.push({
|
|
6191
|
+
id: ReasoningEffortId(effort.id),
|
|
6192
|
+
name: effort.name,
|
|
6193
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
6194
|
+
});
|
|
6195
|
+
}
|
|
6196
|
+
if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
|
|
6197
|
+
return {
|
|
6198
|
+
efforts,
|
|
6199
|
+
...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
|
|
6200
|
+
};
|
|
6201
|
+
}
|
|
6202
|
+
/** Validate one persisted model, or undefined when malformed. */
|
|
6203
|
+
function sanitizeModel(value) {
|
|
6204
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
6205
|
+
const raw = value;
|
|
6206
|
+
if (typeof raw.id !== "string" || raw.id.length === 0 || typeof raw.name !== "string" || raw.name.length === 0 || raw.description !== void 0 && typeof raw.description !== "string" || raw.contextWindow !== void 0 && (typeof raw.contextWindow !== "number" || !Number.isInteger(raw.contextWindow) || raw.contextWindow <= 0) || raw.maxContextWindow !== void 0 && (typeof raw.maxContextWindow !== "number" || !Number.isSafeInteger(raw.maxContextWindow) || raw.maxContextWindow <= 0) || raw.maxOutputTokens !== void 0 && (typeof raw.maxOutputTokens !== "number" || !Number.isSafeInteger(raw.maxOutputTokens) || raw.maxOutputTokens <= 0) || raw.priority !== void 0 && (typeof raw.priority !== "number" || !Number.isFinite(raw.priority))) return void 0;
|
|
6207
|
+
const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
|
|
6208
|
+
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
6209
|
+
const thinkingType = raw.thinkingType;
|
|
6210
|
+
if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
|
|
6211
|
+
const fastTier = raw.fastTier;
|
|
6212
|
+
if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
|
|
6213
|
+
const copilotWire = raw.copilotWire;
|
|
6214
|
+
if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
|
|
6215
|
+
const copilotResponses = raw.copilotResponses;
|
|
6216
|
+
if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
|
|
6217
|
+
const inputModalities = raw.inputModalities;
|
|
6218
|
+
if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
|
|
6219
|
+
return {
|
|
6220
|
+
id: raw.id,
|
|
6221
|
+
name: raw.name,
|
|
6222
|
+
...raw.description === void 0 ? {} : { description: raw.description },
|
|
6223
|
+
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
6224
|
+
...raw.maxContextWindow === void 0 ? {} : { maxContextWindow: raw.maxContextWindow },
|
|
6225
|
+
...raw.maxOutputTokens === void 0 ? {} : { maxOutputTokens: raw.maxOutputTokens },
|
|
6226
|
+
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
6227
|
+
...reasoning === void 0 ? {} : { reasoning },
|
|
6228
|
+
...thinkingType === void 0 ? {} : { thinkingType },
|
|
6229
|
+
...fastTier === void 0 ? {} : { fastTier },
|
|
6230
|
+
...copilotWire === void 0 ? {} : { copilotWire },
|
|
6231
|
+
...copilotResponses === void 0 ? {} : { copilotResponses },
|
|
6232
|
+
...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
|
|
6233
|
+
};
|
|
6234
|
+
}
|
|
6235
|
+
/**
|
|
6236
|
+
* Validate one persisted snapshot. Strict: any malformed field drops the
|
|
6237
|
+
* whole snapshot rather than repairing it — the next successful discovery
|
|
6238
|
+
* rewrites the entry anyway.
|
|
6239
|
+
* @param value - the raw per-provider file entry.
|
|
6240
|
+
* @returns the validated snapshot, or undefined when unusable.
|
|
6241
|
+
*/
|
|
6242
|
+
function sanitizeSnapshot(value) {
|
|
6243
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
6244
|
+
const raw = value;
|
|
6245
|
+
if (typeof raw.at !== "number" || !Number.isFinite(raw.at)) return void 0;
|
|
6246
|
+
if (!Array.isArray(raw.models) || raw.models.length === 0) return void 0;
|
|
6247
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6248
|
+
const models = [];
|
|
6249
|
+
for (const entry of raw.models) {
|
|
6250
|
+
const model = sanitizeModel(entry);
|
|
6251
|
+
if (model === void 0 || seen.has(model.id)) return void 0;
|
|
6252
|
+
seen.add(model.id);
|
|
6253
|
+
models.push(model);
|
|
6254
|
+
}
|
|
6255
|
+
return {
|
|
6256
|
+
at: raw.at,
|
|
6257
|
+
models
|
|
6258
|
+
};
|
|
6259
|
+
}
|
|
6260
|
+
/** Read the whole file; missing or unparsable reads as an empty cache. */
|
|
6261
|
+
async function readCatalogFile(path) {
|
|
6262
|
+
let text;
|
|
6263
|
+
try {
|
|
6264
|
+
text = await readFile(path, "utf8");
|
|
6265
|
+
} catch {
|
|
6266
|
+
return {};
|
|
6267
|
+
}
|
|
6268
|
+
try {
|
|
6269
|
+
const parsed = JSON.parse(text);
|
|
6270
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
6271
|
+
return parsed;
|
|
6272
|
+
} catch {
|
|
6273
|
+
return {};
|
|
6274
|
+
}
|
|
6275
|
+
}
|
|
6276
|
+
/** Persist the whole file atomically (tmp file + rename). */
|
|
6277
|
+
async function writeCatalogFile(store, path) {
|
|
6278
|
+
await mkdir(dirname(path), { recursive: true });
|
|
6279
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
6280
|
+
try {
|
|
6281
|
+
await writeFile(tmp, JSON.stringify(store, null, 2));
|
|
6282
|
+
await rename(tmp, path);
|
|
6283
|
+
} catch (error) {
|
|
6284
|
+
await rm(tmp, { force: true });
|
|
6285
|
+
throw error;
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
/**
|
|
6289
|
+
* Like the auth store, mutations of one resolved path share an in-process
|
|
6290
|
+
* write chain in call order. Each reads after the preceding mutation settles;
|
|
6291
|
+
* a failure rejects its caller without blocking later mutations. Idle chains
|
|
6292
|
+
* are removed. Separate processes (or filesystem aliases of a path) are not
|
|
6293
|
+
* locked and remain last-writer-wins.
|
|
6294
|
+
*/
|
|
6295
|
+
const writeChains = /* @__PURE__ */ new Map();
|
|
6296
|
+
async function serialize(path, action) {
|
|
6297
|
+
const key = resolve(path);
|
|
6298
|
+
const next = (writeChains.get(key) ?? Promise.resolve()).then(action, action);
|
|
6299
|
+
const tail = next.then(() => void 0, () => void 0);
|
|
6300
|
+
writeChains.set(key, tail);
|
|
6301
|
+
try {
|
|
6302
|
+
return await next;
|
|
6303
|
+
} finally {
|
|
6304
|
+
if (writeChains.get(key) === tail) writeChains.delete(key);
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
/**
|
|
6308
|
+
* Build the durable half of one provider's catalog cache over the shared
|
|
6309
|
+
* models.json file. Saves and clears serialize with all accounts at this path
|
|
6310
|
+
* in this process; readers see a complete file before or after an atomic rename.
|
|
6311
|
+
* @param provider - the provider route keying the file entry.
|
|
6312
|
+
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
6313
|
+
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
6314
|
+
*/
|
|
6315
|
+
function catalogStore(provider, path = modelsFilePath()) {
|
|
6316
|
+
return {
|
|
6317
|
+
async load() {
|
|
6318
|
+
return sanitizeSnapshot((await readCatalogFile(path))[provider]);
|
|
6319
|
+
},
|
|
6320
|
+
async save(snapshot) {
|
|
6321
|
+
return serialize(path, async () => {
|
|
6322
|
+
const store = await readCatalogFile(path);
|
|
6323
|
+
store[provider] = snapshot;
|
|
6324
|
+
await writeCatalogFile(store, path);
|
|
6325
|
+
});
|
|
6326
|
+
},
|
|
6327
|
+
async clear() {
|
|
6328
|
+
return serialize(path, async () => {
|
|
6329
|
+
const store = await readCatalogFile(path);
|
|
6330
|
+
if (store[provider] === void 0) return;
|
|
6331
|
+
delete store[provider];
|
|
6332
|
+
await writeCatalogFile(store, path);
|
|
6333
|
+
});
|
|
6334
|
+
}
|
|
6335
|
+
};
|
|
6336
|
+
}
|
|
6337
|
+
/**
|
|
6338
|
+
* The durable half of one NON-default account's catalog cache. The provider
|
|
6339
|
+
* entry belongs to the default account; every other account keeps its own
|
|
6340
|
+
* snapshot under `accounts[provider][account]`, so a working secondary login
|
|
6341
|
+
* survives restarts and network failures even when the default login is dead.
|
|
6342
|
+
* Saves and clears share {@link catalogStore}'s in-process write chain.
|
|
6343
|
+
* @param provider - the provider route.
|
|
6344
|
+
* @param account - the canonical account key.
|
|
6345
|
+
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
6346
|
+
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
6347
|
+
*/
|
|
6348
|
+
function accountCatalogStore(provider, account, path = modelsFilePath()) {
|
|
6349
|
+
return {
|
|
6350
|
+
async load() {
|
|
6351
|
+
const section = accountSection(await readCatalogFile(path), provider);
|
|
6352
|
+
return section !== void 0 && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : void 0;
|
|
6353
|
+
},
|
|
6354
|
+
async save(snapshot) {
|
|
6355
|
+
return serialize(path, async () => {
|
|
6356
|
+
const store = await readCatalogFile(path);
|
|
6357
|
+
const accounts = typeof store.accounts === "object" && store.accounts !== null && !Array.isArray(store.accounts) ? store.accounts : {};
|
|
6358
|
+
const section = {
|
|
6359
|
+
...accountSection(store, provider) ?? {},
|
|
6360
|
+
[account]: snapshot
|
|
6361
|
+
};
|
|
6362
|
+
store.accounts = {
|
|
6363
|
+
...accounts,
|
|
6364
|
+
[provider]: section
|
|
6365
|
+
};
|
|
6366
|
+
await writeCatalogFile(store, path);
|
|
6367
|
+
});
|
|
6368
|
+
},
|
|
6369
|
+
async clear() {
|
|
6370
|
+
return serialize(path, async () => {
|
|
6371
|
+
const store = await readCatalogFile(path);
|
|
6372
|
+
const section = accountSection(store, provider);
|
|
6373
|
+
if (section === void 0 || !Object.hasOwn(section, account)) return;
|
|
6374
|
+
const rest = { ...section };
|
|
6375
|
+
delete rest[account];
|
|
6376
|
+
store.accounts = {
|
|
6377
|
+
...store.accounts,
|
|
6378
|
+
[provider]: rest
|
|
6379
|
+
};
|
|
6380
|
+
await writeCatalogFile(store, path);
|
|
6381
|
+
});
|
|
6382
|
+
}
|
|
6383
|
+
};
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
//#endregion
|
|
6387
|
+
//#region src/translate/resolved.ts
|
|
6344
6388
|
/**
|
|
6345
|
-
*
|
|
6346
|
-
*
|
|
6347
|
-
*
|
|
6348
|
-
*
|
|
6389
|
+
* Wires with text-only tool outputs receive images in a following user turn.
|
|
6390
|
+
* Defer that turn until all consecutive user messages have been processed:
|
|
6391
|
+
* parallel tool results can arrive in separate harness messages, and a user
|
|
6392
|
+
* image message must not interrupt their tool-call/output pairing.
|
|
6349
6393
|
*/
|
|
6350
|
-
|
|
6351
|
-
const
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6394
|
+
function withToolResultImages(messages) {
|
|
6395
|
+
const out = [];
|
|
6396
|
+
let images = [];
|
|
6397
|
+
const flush = () => {
|
|
6398
|
+
if (images.length > 0) out.push({
|
|
6399
|
+
role: "user",
|
|
6400
|
+
content: images
|
|
6401
|
+
});
|
|
6402
|
+
images = [];
|
|
6403
|
+
};
|
|
6404
|
+
for (const message of messages) {
|
|
6405
|
+
if (message.role === "assistant") flush();
|
|
6406
|
+
out.push(message);
|
|
6407
|
+
if (message.role === "tool") {
|
|
6408
|
+
const parts = message.content.filter((part) => part.type === "image" && "dataBase64" in part);
|
|
6409
|
+
if (parts.length > 0) images.push({
|
|
6410
|
+
type: "text",
|
|
6411
|
+
text: `Images from tool result ${String(message.toolCallId)}:`
|
|
6412
|
+
}, ...parts);
|
|
6413
|
+
}
|
|
6414
|
+
for (const block of message.content) {
|
|
6415
|
+
if (block.type !== "tool-result") continue;
|
|
6416
|
+
const parts = block.content.filter((part) => part.type === "image" && "dataBase64" in part);
|
|
6417
|
+
if (parts.length > 0) images.push({
|
|
6418
|
+
type: "text",
|
|
6419
|
+
text: `Images from tool result ${String(block.toolCallId)}:`
|
|
6420
|
+
}, ...parts);
|
|
6358
6421
|
}
|
|
6359
|
-
yield* translator.push(event);
|
|
6360
|
-
if (translator.terminated) return;
|
|
6361
6422
|
}
|
|
6362
|
-
|
|
6423
|
+
flush();
|
|
6424
|
+
return out;
|
|
6425
|
+
}
|
|
6426
|
+
/**
|
|
6427
|
+
* Failure code a host's image-offload executor answers by replacing the oldest
|
|
6428
|
+
* retained images with text and retrying (`IMAGE_OFFLOAD_REQUIRED_CODE` in
|
|
6429
|
+
* dsh-llm 0.2+). Spelled out so the build links against older hosts too.
|
|
6430
|
+
*/
|
|
6431
|
+
const IMAGE_OFFLOAD_REQUIRED = "IMAGE_OFFLOAD_REQUIRED";
|
|
6432
|
+
/**
|
|
6433
|
+
* Whether the installed host records image offloads. Hosts that predate the
|
|
6434
|
+
* executor never mark images `offloaded`, so asking them to would fail every
|
|
6435
|
+
* later request instead of shrinking it.
|
|
6436
|
+
*/
|
|
6437
|
+
function hostSupportsImageOffload() {
|
|
6438
|
+
const exports = llm;
|
|
6439
|
+
return exports["IMAGE_OFFLOAD_REQUIRED_CODE"] === IMAGE_OFFLOAD_REQUIRED && typeof exports["offloadedImageText"] === "function";
|
|
6440
|
+
}
|
|
6441
|
+
/**
|
|
6442
|
+
* How many of the oldest images still sent inline must be offloaded before
|
|
6443
|
+
* the resolved request fits `maxBase64Bytes` of image data; zero when it fits.
|
|
6444
|
+
* Images are counted depth-first in request order, skipping assistant
|
|
6445
|
+
* messages, the same order the host's executor selects occurrences in.
|
|
6446
|
+
* @param messages - the resolved request, after {@link resolveImages}.
|
|
6447
|
+
* @param maxBase64Bytes - the route's budget for all inline image data.
|
|
6448
|
+
*/
|
|
6449
|
+
function requiredImageOffloadCount(messages, maxBase64Bytes) {
|
|
6450
|
+
const lengths = [];
|
|
6451
|
+
const visit = (blocks) => {
|
|
6452
|
+
for (const block of blocks) if (block.type === "image" && "dataBase64" in block) lengths.push(block.dataBase64.length);
|
|
6453
|
+
else if (block.type === "tool-result") visit(block.content);
|
|
6454
|
+
};
|
|
6455
|
+
for (const message of messages) if (message.role !== "assistant") visit(message.content);
|
|
6456
|
+
let excess = lengths.reduce((sum, bytes) => sum + bytes, 0) - maxBase64Bytes;
|
|
6457
|
+
let count = 0;
|
|
6458
|
+
while (excess > 0 && count < lengths.length) {
|
|
6459
|
+
excess -= lengths[count];
|
|
6460
|
+
count += 1;
|
|
6461
|
+
}
|
|
6462
|
+
return count;
|
|
6463
|
+
}
|
|
6464
|
+
/**
|
|
6465
|
+
* The request projection for one oversized image, or undefined when it fits.
|
|
6466
|
+
* Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
|
|
6467
|
+
* target edges (`width`/`height`); each validates only its own fields, so
|
|
6468
|
+
* one projection carries both.
|
|
6469
|
+
*/
|
|
6470
|
+
function imageRequestTarget(ref, limit) {
|
|
6471
|
+
const longEdge = Math.max(ref.width, ref.height);
|
|
6472
|
+
if (!(longEdge > limit.maxEdge)) return void 0;
|
|
6473
|
+
const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
|
|
6474
|
+
const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
|
|
6475
|
+
const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
|
|
6476
|
+
return {
|
|
6477
|
+
width,
|
|
6478
|
+
height,
|
|
6479
|
+
maxPixels: width * height,
|
|
6480
|
+
maxBytes: limit.maxBytes
|
|
6481
|
+
};
|
|
6482
|
+
}
|
|
6483
|
+
/**
|
|
6484
|
+
* Resolve every ImageBlock's attachment reference to inline base64 bytes.
|
|
6485
|
+
* Messages without images pass through unchanged. A request carrying an image
|
|
6486
|
+
* with no attachment service available fails loudly rather than silently
|
|
6487
|
+
* dropping the image.
|
|
6488
|
+
* @param messages - the request's conversation messages.
|
|
6489
|
+
* @param attachments - the deployment's attachment service, when mounted.
|
|
6490
|
+
* @param signal - cancellation for the storage reads.
|
|
6491
|
+
* @param limit - the route's outgoing image cap; oversized images are sent
|
|
6492
|
+
* downscaled while the stored attachment and history stay untouched.
|
|
6493
|
+
* @returns the same messages with image blocks resolved for the translators.
|
|
6494
|
+
*/
|
|
6495
|
+
async function resolveImages(messages, attachments, signal, limit) {
|
|
6496
|
+
const hasRetainedImage = (block) => block.type === "image" ? !("offloaded" in block && block.offloaded === true) : block.type === "tool-result" && block.content.some(hasRetainedImage);
|
|
6497
|
+
if (attachments === void 0 && messages.some((message) => message.content.some(hasRetainedImage))) throw new LlmError("dsh-subscriptions: the request carries an image but no attachments service is mounted; image input requires the harness attachment store", "UNSUPPORTED");
|
|
6498
|
+
const readForRequest = async (ref) => {
|
|
6499
|
+
if (attachments === void 0) throw new Error("attachments service is unavailable");
|
|
6500
|
+
const target = limit === void 0 ? void 0 : imageRequestTarget(ref, limit);
|
|
6501
|
+
if (target !== void 0) try {
|
|
6502
|
+
const version = await attachments.readImageRequest(ref, target, signal);
|
|
6503
|
+
return {
|
|
6504
|
+
data: version.data,
|
|
6505
|
+
mediaType: version.mediaType,
|
|
6506
|
+
ref: version.attachment
|
|
6507
|
+
};
|
|
6508
|
+
} catch (error) {
|
|
6509
|
+
if (signal?.aborted) throw error;
|
|
6510
|
+
}
|
|
6511
|
+
const stored = await attachments.readImage(ref, signal);
|
|
6512
|
+
return {
|
|
6513
|
+
data: stored.data,
|
|
6514
|
+
mediaType: stored.ref.mediaType,
|
|
6515
|
+
ref: stored.ref
|
|
6516
|
+
};
|
|
6517
|
+
};
|
|
6518
|
+
const resolveBlock = async (block) => {
|
|
6519
|
+
if (block.type === "tool-result") return [{
|
|
6520
|
+
...block,
|
|
6521
|
+
content: (await Promise.all(block.content.map(resolveBlock))).flat()
|
|
6522
|
+
}];
|
|
6523
|
+
if (block.type !== "image" || "dataBase64" in block) return [block];
|
|
6524
|
+
if ("offloaded" in block && block.offloaded === true) {
|
|
6525
|
+
const format = llm.offloadedImageText;
|
|
6526
|
+
return [{
|
|
6527
|
+
type: "text",
|
|
6528
|
+
text: format?.(block.attachment) ?? `[image omitted to fit request image limits; ${block.attachment.attachmentId}]`
|
|
6529
|
+
}];
|
|
6530
|
+
}
|
|
6531
|
+
const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
|
|
6532
|
+
const { attachmentId, mediaType, bytes, width, height, name: name$1 } = ref;
|
|
6533
|
+
return [{
|
|
6534
|
+
type: "image",
|
|
6535
|
+
mediaType: sentType,
|
|
6536
|
+
dataBase64: Buffer.from(data).toString("base64")
|
|
6537
|
+
}, {
|
|
6538
|
+
type: "text",
|
|
6539
|
+
text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
|
|
6540
|
+
attachmentId,
|
|
6541
|
+
mediaType,
|
|
6542
|
+
bytes,
|
|
6543
|
+
width,
|
|
6544
|
+
height,
|
|
6545
|
+
...name$1 === void 0 ? {} : { name: name$1 }
|
|
6546
|
+
})}`
|
|
6547
|
+
}];
|
|
6548
|
+
};
|
|
6549
|
+
return Promise.all(messages.map(async (message) => ({
|
|
6550
|
+
role: message.role,
|
|
6551
|
+
...message.source === void 0 ? {} : { source: message.source },
|
|
6552
|
+
...message.role === "tool" ? {
|
|
6553
|
+
toolCallId: message.toolCallId,
|
|
6554
|
+
isError: message.isError
|
|
6555
|
+
} : {},
|
|
6556
|
+
content: (await Promise.all(message.content.map(resolveBlock))).flat()
|
|
6557
|
+
})));
|
|
6363
6558
|
}
|
|
6364
6559
|
|
|
6365
6560
|
//#endregion
|
|
@@ -6490,8 +6685,8 @@ const CLAUDE_RESET_FIELDS = [
|
|
|
6490
6685
|
* headers are deliberately not read: they are rollover snapshots attached to
|
|
6491
6686
|
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
6492
6687
|
* earliest of them is typically the bucket that still had room — a wait that
|
|
6493
|
-
* lands straight back in the closed window.
|
|
6494
|
-
*
|
|
6688
|
+
* lands straight back in the closed window. The parsed reset supplies the retry
|
|
6689
|
+
* hint; diagnostics omit raw provider headers and body text.
|
|
6495
6690
|
*/
|
|
6496
6691
|
const claudeRateLimitReset = (response, body, now) => {
|
|
6497
6692
|
const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
|
|
@@ -6686,11 +6881,49 @@ function isClaudePermanentRefreshError(error) {
|
|
|
6686
6881
|
return error instanceof OAuthEndpointError && (error.oauthCode === "invalid_grant" || error.oauthCode === "invalid_token");
|
|
6687
6882
|
}
|
|
6688
6883
|
const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
6689
|
-
/**
|
|
6884
|
+
/** ISO-8601 or numeric epoch (seconds below 1e10 in magnitude, otherwise ms). */
|
|
6690
6885
|
function claudeResetsAt(value) {
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6886
|
+
let parsed;
|
|
6887
|
+
if (typeof value === "number") parsed = Math.trunc(Math.abs(value) < 1e10 ? value * 1e3 : value);
|
|
6888
|
+
else if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) parsed = Date.parse(value);
|
|
6889
|
+
else return;
|
|
6890
|
+
return Number.isFinite(parsed) && Number.isFinite(new Date(parsed).getTime()) ? parsed : void 0;
|
|
6891
|
+
}
|
|
6892
|
+
/**
|
|
6893
|
+
* Read the observed cedar_ember shape from ItsJazii/pane, not an official
|
|
6894
|
+
* contract. Each entry retains its grant id and counts; claimable is only
|
|
6895
|
+
* read-only server-selection metadata, never an implemented redemption path.
|
|
6896
|
+
*/
|
|
6897
|
+
function claudeResetCredits(value, now) {
|
|
6898
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
6899
|
+
const ember = value;
|
|
6900
|
+
if (ember.eligible !== true || !Array.isArray(ember.grants)) return void 0;
|
|
6901
|
+
const cooldownUntil = claudeResetsAt(ember.cooldown_until);
|
|
6902
|
+
const cooling = cooldownUntil === void 0 ? ember.cooldown_until !== void 0 && ember.cooldown_until !== null : cooldownUntil > now;
|
|
6903
|
+
const credits = [];
|
|
6904
|
+
let selected = false;
|
|
6905
|
+
for (const raw of ember.grants) {
|
|
6906
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
6907
|
+
const grant = raw;
|
|
6908
|
+
const { id, resets_total: total, resets_left: left, usable_now: usable, paused } = grant;
|
|
6909
|
+
const expiresAt = claudeResetsAt(grant.ends_at);
|
|
6910
|
+
if (typeof id !== "string" || id.trim().length === 0 || typeof total !== "number" || !Number.isSafeInteger(total) || total < 1 || typeof left !== "number" || !Number.isSafeInteger(left) || expiresAt === void 0 || usable !== void 0 && typeof usable !== "boolean" || paused !== void 0 && typeof paused !== "boolean") continue;
|
|
6911
|
+
const resetsLeft = Math.max(0, Math.min(left, total));
|
|
6912
|
+
if (resetsLeft === 0) continue;
|
|
6913
|
+
const claimable = !selected && id === ember.next_grant_id && usable === true && paused !== true && !cooling && expiresAt > now;
|
|
6914
|
+
if (claimable) selected = true;
|
|
6915
|
+
credits.push({
|
|
6916
|
+
id,
|
|
6917
|
+
expiresAt,
|
|
6918
|
+
resetsTotal: total,
|
|
6919
|
+
resetsLeft,
|
|
6920
|
+
...usable === void 0 ? {} : { usableNow: usable },
|
|
6921
|
+
...paused === void 0 ? {} : { paused },
|
|
6922
|
+
claimable,
|
|
6923
|
+
...cooldownUntil === void 0 ? {} : { cooldownUntil }
|
|
6924
|
+
});
|
|
6925
|
+
}
|
|
6926
|
+
return credits.length > 0 ? credits : void 0;
|
|
6694
6927
|
}
|
|
6695
6928
|
/** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
|
|
6696
6929
|
function claudeWindowTiming(kind) {
|
|
@@ -6742,6 +6975,10 @@ function claudeLimitsWindows(value) {
|
|
|
6742
6975
|
* source of Claude Code's `/usage` screen). Newer responses carry a
|
|
6743
6976
|
* structured `limits` array; older ones the flat `five_hour`/`seven_day*`
|
|
6744
6977
|
* buckets — both shapes are read, the array winning when it has entries.
|
|
6978
|
+
* Requests the optional cedar_ember banked-reset block with the existing CLI
|
|
6979
|
+
* identity. A 400/403 retries once without that parameter; ordinary windows
|
|
6980
|
+
* remain available and resetCreditsError records the optional lookup failure.
|
|
6981
|
+
* Missing/ineligible blocks and empty or unusable grants expose no credits.
|
|
6745
6982
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
6746
6983
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
6747
6984
|
* @param signal - caller cancellation from the RPC transport.
|
|
@@ -6750,7 +6987,7 @@ function claudeLimitsWindows(value) {
|
|
|
6750
6987
|
*/
|
|
6751
6988
|
async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
|
|
6752
6989
|
const userAgent = claudeCliUserAgent(await cliVersion());
|
|
6753
|
-
const
|
|
6990
|
+
const init = {
|
|
6754
6991
|
headers: {
|
|
6755
6992
|
"authorization": `Bearer ${session.accessToken}`,
|
|
6756
6993
|
"anthropic-beta": "oauth-2025-04-20",
|
|
@@ -6758,13 +6995,25 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
|
|
|
6758
6995
|
"accept": "application/json"
|
|
6759
6996
|
},
|
|
6760
6997
|
...signal === void 0 ? {} : { signal }
|
|
6761
|
-
}
|
|
6998
|
+
};
|
|
6999
|
+
let response = await fetchFn(`${CLAUDE_USAGE_URL}?cedar_ember=1`, init);
|
|
7000
|
+
let resetCreditsError;
|
|
7001
|
+
if (response.status === 400 || response.status === 403) {
|
|
7002
|
+
resetCreditsError = `Claude banked resets lookup failed (HTTP ${response.status}); using plain usage.`;
|
|
7003
|
+
response = await fetchFn(CLAUDE_USAGE_URL, init);
|
|
7004
|
+
}
|
|
6762
7005
|
if (!response.ok) throw await oauthEndpointError(response, "claude usage");
|
|
6763
7006
|
const payload = await response.json();
|
|
7007
|
+
const resetCredits = resetCreditsError === void 0 ? claudeResetCredits(payload.cedar_ember, Date.now()) : void 0;
|
|
7008
|
+
const resets = {
|
|
7009
|
+
...resetCredits === void 0 ? {} : { resetCredits },
|
|
7010
|
+
...resetCreditsError === void 0 ? {} : { resetCreditsError }
|
|
7011
|
+
};
|
|
6764
7012
|
const modern = claudeLimitsWindows(payload.limits);
|
|
6765
7013
|
if (modern.length > 0) return {
|
|
6766
7014
|
supported: true,
|
|
6767
|
-
windows: modern
|
|
7015
|
+
windows: modern,
|
|
7016
|
+
...resets
|
|
6768
7017
|
};
|
|
6769
7018
|
const windows = [];
|
|
6770
7019
|
const legacy = [
|
|
@@ -6776,7 +7025,8 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
|
|
|
6776
7025
|
for (const window of legacy) if (window !== void 0) windows.push(window);
|
|
6777
7026
|
return {
|
|
6778
7027
|
supported: true,
|
|
6779
|
-
windows
|
|
7028
|
+
windows,
|
|
7029
|
+
...resets
|
|
6780
7030
|
};
|
|
6781
7031
|
}
|
|
6782
7032
|
function claudeThinkingType(capabilities) {
|
|
@@ -6861,6 +7111,14 @@ const CLAUDE_IMAGE_LIMIT = {
|
|
|
6861
7111
|
maxEdge: 2e3,
|
|
6862
7112
|
maxBytes: 375e4
|
|
6863
7113
|
};
|
|
7114
|
+
/**
|
|
7115
|
+
* Inline image data a Claude request may carry, in base64 bytes. Every turn
|
|
7116
|
+
* resends the history, so screenshots accumulate until the request passes the
|
|
7117
|
+
* API's 32MB body limit and every later turn and compaction fails with HTTP
|
|
7118
|
+
* 413 `request_too_large`. Past this budget the host offloads the oldest
|
|
7119
|
+
* images to text and retries; the remaining 12MB holds prompts, tools, and text.
|
|
7120
|
+
*/
|
|
7121
|
+
const CLAUDE_REQUEST_IMAGE_BUDGET = 20 * 1024 * 1024;
|
|
6864
7122
|
/** The Claude 4.5 family accepts image input. */
|
|
6865
7123
|
const CLAUDE_MODALITIES = ["text", "image"];
|
|
6866
7124
|
/**
|
|
@@ -6876,15 +7134,20 @@ const CLAUDE_MODALITIES = ["text", "image"];
|
|
|
6876
7134
|
* @param maxTokens - the resolved output cap.
|
|
6877
7135
|
* @param thinking - the thinking parameter, when the model takes one.
|
|
6878
7136
|
* @param effort - the reasoning effort, when the model advertises efforts.
|
|
7137
|
+
* @param cacheTtl - lifetime of every cache breakpoint; defaults to five minutes.
|
|
7138
|
+
* Requests that carry a `purpose` (compaction, session titles) always use five
|
|
7139
|
+
* minutes to preserve their existing auxiliary-request behavior. A five-minute
|
|
7140
|
+
* mark can still read an entry a one-hour mark wrote.
|
|
6879
7141
|
* @returns the JSON body to POST.
|
|
6880
7142
|
*/
|
|
6881
|
-
function claudeRequestBody(options, messages, maxTokens, thinking, effort) {
|
|
7143
|
+
function claudeRequestBody(options, messages, maxTokens, thinking, effort, cacheTtl) {
|
|
7144
|
+
const ttl = options.purpose === void 0 ? cacheTtl : void 0;
|
|
6882
7145
|
const anthropicMessages = toAnthropicMessages(messages);
|
|
6883
|
-
markMessageCache(anthropicMessages);
|
|
7146
|
+
markMessageCache(anthropicMessages, ttl);
|
|
6884
7147
|
return {
|
|
6885
7148
|
model: options.model,
|
|
6886
7149
|
max_tokens: maxTokens,
|
|
6887
|
-
system: toAnthropicSystem(options.system, messages),
|
|
7150
|
+
system: toAnthropicSystem(options.system, messages, ttl),
|
|
6888
7151
|
messages: anthropicMessages,
|
|
6889
7152
|
...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
|
|
6890
7153
|
...thinking === void 0 ? {} : { thinking },
|
|
@@ -7070,9 +7333,13 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
7070
7333
|
}
|
|
7071
7334
|
async request(options, session, signal) {
|
|
7072
7335
|
const messages = await resolveImages(options.messages, this.options.resolveAttachments?.(), signal, CLAUDE_IMAGE_LIMIT);
|
|
7336
|
+
if (hostSupportsImageOffload()) {
|
|
7337
|
+
const offloadImages = requiredImageOffloadCount(messages, CLAUDE_REQUEST_IMAGE_BUDGET);
|
|
7338
|
+
if (offloadImages > 0) throw new LlmError(`claude request images exceed the ${CLAUDE_REQUEST_IMAGE_BUDGET}-byte base64 budget; ${offloadImages} more oldest image(s) must be offloaded`, IMAGE_OFFLOAD_REQUIRED, { offloadImages });
|
|
7339
|
+
}
|
|
7073
7340
|
const disc = await this.discovered(options.model);
|
|
7074
7341
|
const maxTokens = options.maxTokens ?? claudeMaxTokens(this.options.models.find((entry) => entry.id === options.model), disc);
|
|
7075
|
-
const body = claudeRequestBody(options, messages, maxTokens, this.thinkingParam(disc?.thinkingType, maxTokens), options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? String(options.reasoningEffort) : void 0);
|
|
7342
|
+
const body = claudeRequestBody(options, messages, maxTokens, this.thinkingParam(disc?.thinkingType, maxTokens), options.reasoningEffort !== void 0 && disc?.reasoning !== void 0 ? String(options.reasoningEffort) : void 0, this.options.promptCacheTtl);
|
|
7076
7343
|
const cliVersion = await (this.options.resolveCliVersion ?? localClaudeCliVersion)();
|
|
7077
7344
|
return hostFetch(CLAUDE_API_URL, {
|
|
7078
7345
|
method: "POST",
|
|
@@ -7266,11 +7533,11 @@ function mapResponsesUsage(usage) {
|
|
|
7266
7533
|
* @returns the mapped error (context overflow, quota, otherwise SERVER).
|
|
7267
7534
|
*/
|
|
7268
7535
|
function responsesFailure(code, message) {
|
|
7269
|
-
const text = message ?? code ?? "the provider reported a failed response";
|
|
7270
7536
|
const detail = `${code ?? ""} ${message ?? ""}`;
|
|
7271
|
-
|
|
7272
|
-
if (code
|
|
7273
|
-
|
|
7537
|
+
let classification = "SERVER";
|
|
7538
|
+
if (code === "context_window_exceeded" || isContextWindowExceededError(detail)) classification = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
7539
|
+
else if (code !== void 0 && /insufficient|quota/i.test(code) || isQuotaExceededError(detail)) classification = QUOTA_EXCEEDED_CODE;
|
|
7540
|
+
return new LlmError(`Responses failure (${classification}): [provider response body omitted]`, classification);
|
|
7274
7541
|
}
|
|
7275
7542
|
/** Assemble the final ContentBlock for one open block. */
|
|
7276
7543
|
function closeBlock$1(block) {
|
|
@@ -7489,7 +7756,7 @@ async function* streamResponses(stream, onActivity, transform) {
|
|
|
7489
7756
|
try {
|
|
7490
7757
|
event = JSON.parse(sseEvent.data);
|
|
7491
7758
|
} catch {
|
|
7492
|
-
throw new LlmError(
|
|
7759
|
+
throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
7493
7760
|
}
|
|
7494
7761
|
if (transform !== void 0) event = transform(event);
|
|
7495
7762
|
yield* translator.push(event);
|
|
@@ -7528,8 +7795,8 @@ const CODEX_RESET_FIELDS = [
|
|
|
7528
7795
|
* rollover snapshots the backend attaches to every response, one per window,
|
|
7529
7796
|
* so they say nothing about which window refused: a burst 429 that would clear
|
|
7530
7797
|
* in seconds still carries a primary rollover hours out, and reading it would
|
|
7531
|
-
* park the turn for those hours.
|
|
7532
|
-
*
|
|
7798
|
+
* park the turn for those hours. The parsed body reset supplies the retry hint;
|
|
7799
|
+
* diagnostics omit raw provider headers and body text.
|
|
7533
7800
|
*/
|
|
7534
7801
|
const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
7535
7802
|
/** Default instruction when the request carries no system prompt. */
|
|
@@ -7701,6 +7968,63 @@ function isCodexPermanentRefreshError(error) {
|
|
|
7701
7968
|
return error instanceof OAuthEndpointError && error.oauthCode !== void 0 && PERMANENT_REFRESH_CODES.has(error.oauthCode);
|
|
7702
7969
|
}
|
|
7703
7970
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
7971
|
+
const CODEX_RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
7972
|
+
function parseResetDate(value) {
|
|
7973
|
+
if (typeof value !== "string") return void 0;
|
|
7974
|
+
const timestamp = Date.parse(value);
|
|
7975
|
+
return Number.isFinite(timestamp) ? timestamp : void 0;
|
|
7976
|
+
}
|
|
7977
|
+
/** Read banked resets without spending them; consumption uses a separate explicit POST. */
|
|
7978
|
+
async function fetchCodexResetCredits(session, fetchFn = hostFetch, signal) {
|
|
7979
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_URL, {
|
|
7980
|
+
headers: {
|
|
7981
|
+
authorization: `Bearer ${session.accessToken}`,
|
|
7982
|
+
"chatgpt-account-id": session.accountId,
|
|
7983
|
+
originator: "Codex Desktop",
|
|
7984
|
+
"openai-beta": "codex-1",
|
|
7985
|
+
accept: "application/json",
|
|
7986
|
+
...attributionHeaders()
|
|
7987
|
+
},
|
|
7988
|
+
...signal === void 0 ? {} : { signal }
|
|
7989
|
+
});
|
|
7990
|
+
if (!response.ok) throw await oauthEndpointError(response, "codex reset credits");
|
|
7991
|
+
const payload = await response.json();
|
|
7992
|
+
if (!Array.isArray(payload?.credits)) throw new Error("codex reset credits: unexpected response (missing credits array)");
|
|
7993
|
+
return payload.credits.flatMap((value) => {
|
|
7994
|
+
if (typeof value !== "object" || value === null) return [];
|
|
7995
|
+
const credit = value;
|
|
7996
|
+
if (credit.status !== "available" || credit.reset_type !== "codex_rate_limits") return [];
|
|
7997
|
+
const grantedAt = parseResetDate(credit.granted_at);
|
|
7998
|
+
const expiresAt = parseResetDate(credit.expires_at);
|
|
7999
|
+
return [{
|
|
8000
|
+
...typeof credit.id === "string" ? { id: credit.id } : {},
|
|
8001
|
+
...grantedAt === void 0 ? {} : { grantedAt },
|
|
8002
|
+
...expiresAt === void 0 ? {} : { expiresAt }
|
|
8003
|
+
}];
|
|
8004
|
+
});
|
|
8005
|
+
}
|
|
8006
|
+
/** Submit one explicit reset; ambiguous or unconfirmed responses throw without retry. */
|
|
8007
|
+
async function consumeCodexResetCredit(session, creditId, requestId, fetchFn = hostFetch, signal) {
|
|
8008
|
+
const response = await fetchFn(CODEX_RESET_CREDITS_URL + "/consume", {
|
|
8009
|
+
method: "POST",
|
|
8010
|
+
headers: {
|
|
8011
|
+
authorization: "Bearer " + session.accessToken,
|
|
8012
|
+
"chatgpt-account-id": session.accountId,
|
|
8013
|
+
originator: "Codex Desktop",
|
|
8014
|
+
"openai-beta": "codex-1",
|
|
8015
|
+
"content-type": "application/json",
|
|
8016
|
+
accept: "application/json",
|
|
8017
|
+
...attributionHeaders()
|
|
8018
|
+
},
|
|
8019
|
+
body: JSON.stringify({
|
|
8020
|
+
redeem_request_id: requestId,
|
|
8021
|
+
credit_id: creditId
|
|
8022
|
+
}),
|
|
8023
|
+
...signal === void 0 ? {} : { signal }
|
|
8024
|
+
});
|
|
8025
|
+
if (!response.ok) throw await oauthEndpointError(response, "codex consume reset");
|
|
8026
|
+
if ((await response.json())?.code !== "reset") throw new Error("Reset was not confirmed. Verify the account in Codex before retrying.");
|
|
8027
|
+
}
|
|
7704
8028
|
/** Seconds of the canonical 5-hour session and 7-day weekly windows. */
|
|
7705
8029
|
const SESSION_WINDOW_SECONDS = 300 * 60;
|
|
7706
8030
|
const WEEKLY_WINDOW_SECONDS = 10080 * 60;
|
|
@@ -7824,7 +8148,7 @@ async function fetchCodexModels(session, fetchFn = hostFetch, signal, clientVers
|
|
|
7824
8148
|
for (const entry of payload.models) {
|
|
7825
8149
|
if (typeof entry.slug !== "string" || entry.slug.length === 0) continue;
|
|
7826
8150
|
if (entry.visibility === "hide" || entry.visibility === "none") continue;
|
|
7827
|
-
const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0).map((level) => ({
|
|
8151
|
+
const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0 && level.effort !== "ultra").map((level) => ({
|
|
7828
8152
|
id: ReasoningEffortId(level.effort),
|
|
7829
8153
|
name: effortDisplayName(level.effort),
|
|
7830
8154
|
...level.description === void 0 ? {} : { description: level.description }
|
|
@@ -7925,6 +8249,16 @@ function reconcileResponsesToolCalls(input) {
|
|
|
7925
8249
|
return balanced;
|
|
7926
8250
|
}
|
|
7927
8251
|
/**
|
|
8252
|
+
* The `reasoning.effort` wire value for a selected effort. Codex's "ultra" is
|
|
8253
|
+
* a client-side selection (max reasoning plus proactive sub-agent delegation)
|
|
8254
|
+
* that the Codex CLI itself lowers to `max` at the Responses API boundary;
|
|
8255
|
+
* the backend answers HTTP 400 to a literal `ultra`. Discovery no longer
|
|
8256
|
+
* lists it, but an effort saved before that change may still arrive here.
|
|
8257
|
+
*/
|
|
8258
|
+
function codexWireEffort(effort) {
|
|
8259
|
+
return effort === "ultra" ? "max" : effort;
|
|
8260
|
+
}
|
|
8261
|
+
/**
|
|
7928
8262
|
* The Responses request body for one generation. A fast-tier request (the
|
|
7929
8263
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
7930
8264
|
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
@@ -7941,7 +8275,7 @@ function codexRequestBody(options, resolved, fast) {
|
|
|
7941
8275
|
parallel_tool_calls: true
|
|
7942
8276
|
} : {},
|
|
7943
8277
|
...options.reasoningEffort !== void 0 ? { reasoning: {
|
|
7944
|
-
effort: String(options.reasoningEffort),
|
|
8278
|
+
effort: codexWireEffort(String(options.reasoningEffort)),
|
|
7945
8279
|
summary: "auto"
|
|
7946
8280
|
} } : {},
|
|
7947
8281
|
store: false,
|
|
@@ -8374,10 +8708,10 @@ function abortableDelay(ms, signal) {
|
|
|
8374
8708
|
throwIfAborted(signal);
|
|
8375
8709
|
return Promise.resolve();
|
|
8376
8710
|
}
|
|
8377
|
-
return new Promise((resolve$
|
|
8711
|
+
return new Promise((resolve$2, reject) => {
|
|
8378
8712
|
const timer = setTimeout(() => {
|
|
8379
8713
|
cleanup();
|
|
8380
|
-
resolve$
|
|
8714
|
+
resolve$2();
|
|
8381
8715
|
}, ms);
|
|
8382
8716
|
const onAbort = () => {
|
|
8383
8717
|
cleanup();
|
|
@@ -8598,6 +8932,12 @@ var PoolHealthRegistry = class {
|
|
|
8598
8932
|
}
|
|
8599
8933
|
return earliest;
|
|
8600
8934
|
}
|
|
8935
|
+
/** Active failure codes for the given keys; reading them does not change cooldowns. */
|
|
8936
|
+
unavailableReasons(keys, now = Date.now()) {
|
|
8937
|
+
const reasons = [];
|
|
8938
|
+
for (const [key, record$4] of this.records) if (keys.has(key) && record$4.unavailableUntil > now) reasons.push(record$4.reason);
|
|
8939
|
+
return reasons;
|
|
8940
|
+
}
|
|
8601
8941
|
/** Drop records of one provider, or of a single account when given (auth changes). */
|
|
8602
8942
|
clear(provider, account) {
|
|
8603
8943
|
const prefix = account === void 0 ? `${provider}/` : `${provider}/${account}/`;
|
|
@@ -8605,6 +8945,67 @@ var PoolHealthRegistry = class {
|
|
|
8605
8945
|
}
|
|
8606
8946
|
};
|
|
8607
8947
|
|
|
8948
|
+
//#endregion
|
|
8949
|
+
//#region src/providers/replay.ts
|
|
8950
|
+
const REPLAY_KIND = "subscriptions-account-replay";
|
|
8951
|
+
/** Bind opaque replay to the concrete account/model behind a route alias. */
|
|
8952
|
+
async function* streamAccountWithReplay(adapter, options, account, model = options.model) {
|
|
8953
|
+
const key = createHash("sha256").update(JSON.stringify([
|
|
8954
|
+
options.provider,
|
|
8955
|
+
account,
|
|
8956
|
+
model
|
|
8957
|
+
])).digest("hex");
|
|
8958
|
+
const messages = options.messages.map((message) => {
|
|
8959
|
+
if (message.role !== "assistant" || message.source?.replayState === void 0) return message;
|
|
8960
|
+
const source = message.source;
|
|
8961
|
+
if (source.replayState === null || typeof source.replayState !== "object" || !("response" in source.replayState)) return message;
|
|
8962
|
+
const replay = source.replayState;
|
|
8963
|
+
const response = replay.response;
|
|
8964
|
+
if (response !== null && typeof response === "object" && "kind" in response && response.kind === REPLAY_KIND) {
|
|
8965
|
+
if ("version" in response && response.version === 1 && "key" in response && response.key === key && "state" in response) return {
|
|
8966
|
+
...message,
|
|
8967
|
+
source: {
|
|
8968
|
+
...source,
|
|
8969
|
+
provider: options.provider,
|
|
8970
|
+
model,
|
|
8971
|
+
replayState: {
|
|
8972
|
+
...replay,
|
|
8973
|
+
response: response.state
|
|
8974
|
+
}
|
|
8975
|
+
}
|
|
8976
|
+
};
|
|
8977
|
+
const { replayState: _discarded$1,...withoutReplay$1 } = source;
|
|
8978
|
+
return {
|
|
8979
|
+
...message,
|
|
8980
|
+
source: withoutReplay$1
|
|
8981
|
+
};
|
|
8982
|
+
}
|
|
8983
|
+
if (source.provider === options.provider && source.model === model) return message;
|
|
8984
|
+
const { replayState: _discarded,...withoutReplay } = source;
|
|
8985
|
+
return {
|
|
8986
|
+
...message,
|
|
8987
|
+
source: withoutReplay
|
|
8988
|
+
};
|
|
8989
|
+
});
|
|
8990
|
+
for await (const chunk of adapter.streamAccount({
|
|
8991
|
+
...options,
|
|
8992
|
+
model,
|
|
8993
|
+
messages
|
|
8994
|
+
}, account)) if (chunk.type === "finish" && chunk.replayState !== void 0) yield {
|
|
8995
|
+
...chunk,
|
|
8996
|
+
replayState: {
|
|
8997
|
+
...chunk.replayState,
|
|
8998
|
+
response: {
|
|
8999
|
+
kind: REPLAY_KIND,
|
|
9000
|
+
version: 1,
|
|
9001
|
+
key,
|
|
9002
|
+
state: chunk.replayState.response
|
|
9003
|
+
}
|
|
9004
|
+
}
|
|
9005
|
+
};
|
|
9006
|
+
else yield chunk;
|
|
9007
|
+
}
|
|
9008
|
+
|
|
8608
9009
|
//#endregion
|
|
8609
9010
|
//#region src/providers/pool.ts
|
|
8610
9011
|
/** Bound on sticky-session memory; oldest entries evict past it. */
|
|
@@ -8783,34 +9184,39 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8783
9184
|
...modalities === void 0 ? {} : { inputModalities: modalities }
|
|
8784
9185
|
};
|
|
8785
9186
|
}
|
|
9187
|
+
/**
|
|
9188
|
+
* Fail over before output. A single non-quota failure keeps its identity
|
|
9189
|
+
* and shared provider message, with scoped pool recovery or its own hint as fallback.
|
|
9190
|
+
* Multiple failures carry a bounded summary and all attempts in an AggregateError cause.
|
|
9191
|
+
* Pure quota/rate exhaustion retains RATE_LIMIT and this pool's earliest recovery hint.
|
|
9192
|
+
*/
|
|
8786
9193
|
async *stream(options) {
|
|
8787
9194
|
const definition = (await this.pools()).get(poolKey(options.provider, options.model));
|
|
8788
9195
|
if (definition === void 0) throw new LlmError(`unknown pool model "${options.model}"`, "NO_ADAPTER");
|
|
8789
9196
|
const members = await this.concrete(definition.members);
|
|
8790
9197
|
const candidates = await this.select(options.model, members, options.sessionId);
|
|
8791
9198
|
if (candidates.length === 0) throw this.exhausted(options.model, members);
|
|
8792
|
-
|
|
9199
|
+
const failures = [];
|
|
8793
9200
|
for (const member of candidates) {
|
|
8794
9201
|
const adapter = this.options.adapters[member.provider];
|
|
8795
9202
|
if (adapter === void 0) continue;
|
|
8796
|
-
const iterator = adapter
|
|
9203
|
+
const iterator = streamAccountWithReplay(adapter, {
|
|
8797
9204
|
...options,
|
|
8798
|
-
provider: member.provider
|
|
8799
|
-
|
|
8800
|
-
}, member.account)[Symbol.asyncIterator]();
|
|
9205
|
+
provider: member.provider
|
|
9206
|
+
}, member.account, member.model)[Symbol.asyncIterator]();
|
|
8801
9207
|
let first;
|
|
8802
9208
|
try {
|
|
8803
9209
|
first = await iterator.next();
|
|
8804
9210
|
if (first.done === true) throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
|
|
8805
9211
|
} catch (error) {
|
|
8806
9212
|
const classification = classifyPoolFailure(error, member.provider);
|
|
8807
|
-
if (classification.action === "throw") throw error;
|
|
9213
|
+
if (classification.action === "throw" || !(error instanceof LlmError)) throw error;
|
|
8808
9214
|
if ("cooldownMs" in classification) {
|
|
8809
9215
|
this.options.health.markUnavailable(classification.scope === "account" ? accountKey(member.provider, member.account) : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
|
|
8810
9216
|
if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === "RATE_LIMIT") this.options.usage.invalidate(member.provider, member.account);
|
|
8811
9217
|
}
|
|
8812
9218
|
this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output (${error instanceof Error ? error.message : String(error)}); trying the next member`);
|
|
8813
|
-
|
|
9219
|
+
failures.push(error);
|
|
8814
9220
|
continue;
|
|
8815
9221
|
}
|
|
8816
9222
|
this.remember(options.model, options.sessionId, member);
|
|
@@ -8824,7 +9230,7 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8824
9230
|
}
|
|
8825
9231
|
return;
|
|
8826
9232
|
}
|
|
8827
|
-
throw this.exhausted(options.model, members,
|
|
9233
|
+
throw this.exhausted(options.model, members, failures);
|
|
8828
9234
|
}
|
|
8829
9235
|
/**
|
|
8830
9236
|
* Order the candidates for one request. Health filters both strategies;
|
|
@@ -8865,12 +9271,9 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8865
9271
|
}
|
|
8866
9272
|
this.sticky.set(key, memberKey(member.provider, member.account, member.model));
|
|
8867
9273
|
}
|
|
8868
|
-
/**
|
|
8869
|
-
|
|
8870
|
-
|
|
8871
|
-
* hint is scoped to the keys this pool can actually recover through).
|
|
8872
|
-
*/
|
|
8873
|
-
exhausted(model, pool, cause) {
|
|
9274
|
+
/** Report attempt failures or this pool's active cooldowns, never unrelated health records. */
|
|
9275
|
+
exhausted(model, pool, failures = []) {
|
|
9276
|
+
if (pool.length === 0) return new LlmError(`pool "${model}" has no usable member: no account could be resolved`, "NO_ADAPTER");
|
|
8874
9277
|
const keys = /* @__PURE__ */ new Set();
|
|
8875
9278
|
for (const member of pool) {
|
|
8876
9279
|
keys.add(memberKey(member.provider, member.account, member.model));
|
|
@@ -8878,12 +9281,53 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8878
9281
|
}
|
|
8879
9282
|
const recovery = this.options.health.earliestRecovery(keys);
|
|
8880
9283
|
const retryAfterMs$1 = recovery === void 0 ? void 0 : Math.max(recovery - Date.now(), 1);
|
|
8881
|
-
|
|
9284
|
+
const coolingReasons = this.options.health.unavailableReasons(keys);
|
|
9285
|
+
const codes = [...failures.map((error) => error.code), ...coolingReasons];
|
|
9286
|
+
if (codes.length === 0) return new LlmError(`pool "${model}" has no usable member`, "NO_ADAPTER");
|
|
9287
|
+
const quotaOnly = codes.every(isQuotaFailure);
|
|
9288
|
+
if (failures.length === 1 && !isQuotaFailure(failures[0].code)) {
|
|
9289
|
+
const failure$1 = failures[0];
|
|
9290
|
+
if (retryAfterMs$1 !== void 0 && retryAfterMs$1 !== failure$1.failure.providerRetryAfterMs) Object.defineProperty(failure$1, "failure", { value: Object.freeze({
|
|
9291
|
+
...failure$1.failure,
|
|
9292
|
+
providerRetryAfterMs: retryAfterMs$1
|
|
9293
|
+
}) });
|
|
9294
|
+
return failure$1;
|
|
9295
|
+
}
|
|
9296
|
+
const attemptedCodes = failures.map((error) => error.code);
|
|
9297
|
+
const summary = failureSummary([...attemptedCodes, ...coolingReasons.filter((code) => !attemptedCodes.includes(code))]);
|
|
9298
|
+
const cause = failures.length === 0 ? void 0 : failures.length === 1 ? failures[0] : new AggregateError(failures, `${failures.length} pool members failed: ${summary}`);
|
|
9299
|
+
return new LlmError(quotaOnly ? `pool "${model}" exhausted: every member is unavailable or failed` : failures.length > 0 ? `pool "${model}" exhausted: ${failures.length} ${failures.length === 1 ? "member" : "members"} failed (${summary})` : `pool "${model}" unavailable: ${pool.length} members are cooling down (${summary})`, quotaOnly ? "RATE_LIMIT" : attemptedCodes.filter((code) => !isQuotaFailure(code)).at(-1) ?? coolingReasons.filter((code) => !isQuotaFailure(code)).at(-1), {
|
|
8882
9300
|
...retryAfterMs$1 === void 0 ? {} : { providerRetryAfterMs: retryAfterMs$1 },
|
|
8883
9301
|
...cause === void 0 ? {} : { cause }
|
|
8884
9302
|
});
|
|
8885
9303
|
}
|
|
8886
9304
|
};
|
|
9305
|
+
function isQuotaFailure(code) {
|
|
9306
|
+
return code === QUOTA_EXCEEDED_CODE || code === "RATE_LIMIT";
|
|
9307
|
+
}
|
|
9308
|
+
/** Summaries use stable codes only: adapter messages and nested causes can contain raw payloads. */
|
|
9309
|
+
function failureSummary(codes) {
|
|
9310
|
+
const descriptions = {
|
|
9311
|
+
AUTH: "authentication failed",
|
|
9312
|
+
INVALID_CREDENTIAL: "credential is invalid",
|
|
9313
|
+
MISSING_CREDENTIAL: "credential is missing",
|
|
9314
|
+
SERVER: "provider server failed",
|
|
9315
|
+
TIMEOUT: "provider request timed out",
|
|
9316
|
+
EMPTY_RESPONSE: "provider returned no output",
|
|
9317
|
+
TRANSPORT: "provider connection failed",
|
|
9318
|
+
HTTP_402: "provider plan unavailable",
|
|
9319
|
+
HTTP_404: "provider model unavailable",
|
|
9320
|
+
QUOTA: "provider quota exhausted",
|
|
9321
|
+
RATE_LIMIT: "provider rate limit reached"
|
|
9322
|
+
};
|
|
9323
|
+
const limit = 5;
|
|
9324
|
+
const shown = codes.slice(0, limit).map((code) => {
|
|
9325
|
+
const description = descriptions[code];
|
|
9326
|
+
return description === void 0 ? "member unavailable" : `${code}: ${description}`;
|
|
9327
|
+
});
|
|
9328
|
+
if (codes.length > limit) shown.push(`${codes.length - limit} more`);
|
|
9329
|
+
return shown.join("; ");
|
|
9330
|
+
}
|
|
8887
9331
|
function stickyKey(poolId, sessionId) {
|
|
8888
9332
|
return `${String(sessionId)}|${poolId}`;
|
|
8889
9333
|
}
|
|
@@ -9040,10 +9484,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
|
|
|
9040
9484
|
const independent = parseAccountModelId(options.model);
|
|
9041
9485
|
if (independent) {
|
|
9042
9486
|
await this.requireAccount(independent.account, independent.model, true);
|
|
9043
|
-
yield* this.options.adapter.
|
|
9044
|
-
...options,
|
|
9045
|
-
model: independent.model
|
|
9046
|
-
}, independent.account);
|
|
9487
|
+
yield* streamAccountWithReplay(this.options.adapter, options, independent.account, independent.model);
|
|
9047
9488
|
return;
|
|
9048
9489
|
}
|
|
9049
9490
|
const pool = this.options.pool();
|
|
@@ -9051,7 +9492,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
|
|
|
9051
9492
|
yield* pool.stream(options);
|
|
9052
9493
|
return;
|
|
9053
9494
|
}
|
|
9054
|
-
yield* this.options.adapter
|
|
9495
|
+
yield* streamAccountWithReplay(this.options.adapter, options, await this.fallback(options.model));
|
|
9055
9496
|
}
|
|
9056
9497
|
};
|
|
9057
9498
|
|
|
@@ -9200,6 +9641,11 @@ const FALLBACK_HORIZON_MS = {
|
|
|
9200
9641
|
var PoolUsageTracker = class {
|
|
9201
9642
|
entries = /* @__PURE__ */ new Map();
|
|
9202
9643
|
inflight = /* @__PURE__ */ new Map();
|
|
9644
|
+
/** Bumped by {@link invalidate} so a fetch started before it cannot repopulate the cache. */
|
|
9645
|
+
generation = /* @__PURE__ */ new Map();
|
|
9646
|
+
/** Monotonic per-key write order: only the newest settled fetch may cache. */
|
|
9647
|
+
nextSequence = 0;
|
|
9648
|
+
lastWritten = /* @__PURE__ */ new Map();
|
|
9203
9649
|
constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
|
|
9204
9650
|
this.fetcherFor = fetcherFor;
|
|
9205
9651
|
this.ttlMs = ttlMs;
|
|
@@ -9282,13 +9728,25 @@ var PoolUsageTracker = class {
|
|
|
9282
9728
|
throw error;
|
|
9283
9729
|
}
|
|
9284
9730
|
}
|
|
9285
|
-
/**
|
|
9731
|
+
/**
|
|
9732
|
+
* Drop cached snapshots: one account, or a whole provider when `account` is
|
|
9733
|
+
* omitted. A fetch already in flight is abandoned too. It was started against
|
|
9734
|
+
* state the caller has just declared changed — a redeemed credit, a completed
|
|
9735
|
+
* login — so joining it would return the pre-change numbers and cache them for
|
|
9736
|
+
* the full freshness window. Its result is discarded on arrival by the
|
|
9737
|
+
* generation check in {@link refresh}.
|
|
9738
|
+
*/
|
|
9286
9739
|
invalidate(provider, account) {
|
|
9740
|
+
const drop = (key) => {
|
|
9741
|
+
this.entries.delete(key);
|
|
9742
|
+
this.inflight.delete(key);
|
|
9743
|
+
this.generation.set(key, (this.generation.get(key) ?? 0) + 1);
|
|
9744
|
+
};
|
|
9287
9745
|
if (account !== void 0) {
|
|
9288
|
-
|
|
9746
|
+
drop(`${provider}/${account}`);
|
|
9289
9747
|
return;
|
|
9290
9748
|
}
|
|
9291
|
-
for (const key of [...this.entries.keys()]) if (key.startsWith(`${provider}/`))
|
|
9749
|
+
for (const key of [...this.entries.keys(), ...this.inflight.keys()]) if (key.startsWith(`${provider}/`)) drop(key);
|
|
9292
9750
|
}
|
|
9293
9751
|
/**
|
|
9294
9752
|
* Run (or join) the single in-flight fetch for one account key, caching
|
|
@@ -9303,6 +9761,8 @@ var PoolUsageTracker = class {
|
|
|
9303
9761
|
if (pending === void 0) {
|
|
9304
9762
|
const prior = this.entries.get(key);
|
|
9305
9763
|
const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
|
|
9764
|
+
const generation = this.generation.get(key) ?? 0;
|
|
9765
|
+
const sequence = ++this.nextSequence;
|
|
9306
9766
|
pending = fetcher().then((value) => {
|
|
9307
9767
|
const at = Date.now();
|
|
9308
9768
|
const snapshot = {
|
|
@@ -9310,21 +9770,27 @@ var PoolUsageTracker = class {
|
|
|
9310
9770
|
observedAt: at,
|
|
9311
9771
|
stale: false
|
|
9312
9772
|
};
|
|
9313
|
-
this.
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9773
|
+
if ((this.generation.get(key) ?? 0) === generation && (this.lastWritten.get(key) ?? 0) < sequence) {
|
|
9774
|
+
this.lastWritten.set(key, sequence);
|
|
9775
|
+
this.entries.set(key, {
|
|
9776
|
+
snapshot,
|
|
9777
|
+
at
|
|
9778
|
+
});
|
|
9779
|
+
}
|
|
9317
9780
|
return snapshot;
|
|
9318
9781
|
}, (error) => {
|
|
9319
|
-
if (!isMissingOrInvalidCredential(error)) this.
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
|
|
9324
|
-
|
|
9782
|
+
if (!isMissingOrInvalidCredential(error) && (this.generation.get(key) ?? 0) === generation && (this.lastWritten.get(key) ?? 0) < sequence) {
|
|
9783
|
+
this.lastWritten.set(key, sequence);
|
|
9784
|
+
this.entries.set(key, {
|
|
9785
|
+
error,
|
|
9786
|
+
at: Date.now(),
|
|
9787
|
+
cooldownMs: cooldownFor(error, this.ttlMs),
|
|
9788
|
+
...lastSnapshot === void 0 ? {} : { lastSnapshot }
|
|
9789
|
+
});
|
|
9790
|
+
}
|
|
9325
9791
|
throw error;
|
|
9326
9792
|
}).finally(() => {
|
|
9327
|
-
this.inflight.delete(key);
|
|
9793
|
+
if (this.inflight.get(key) === pending) this.inflight.delete(key);
|
|
9328
9794
|
});
|
|
9329
9795
|
this.inflight.set(key, pending);
|
|
9330
9796
|
}
|
|
@@ -9406,8 +9872,8 @@ const GROK_RESET_FIELDS = [
|
|
|
9406
9872
|
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
9407
9873
|
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
9408
9874
|
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
9409
|
-
* which would burn the whole retry budget in seconds.
|
|
9410
|
-
*
|
|
9875
|
+
* which would burn the whole retry budget in seconds. The parsed body reset
|
|
9876
|
+
* supplies the retry hint; diagnostics omit raw provider headers and body text.
|
|
9411
9877
|
*/
|
|
9412
9878
|
const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
|
|
9413
9879
|
/** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
|
|
@@ -10355,7 +10821,7 @@ async function* streamChatCompletions(stream, onActivity) {
|
|
|
10355
10821
|
try {
|
|
10356
10822
|
event = JSON.parse(sseEvent.data);
|
|
10357
10823
|
} catch {
|
|
10358
|
-
throw new LlmError(
|
|
10824
|
+
throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
10359
10825
|
}
|
|
10360
10826
|
yield* translator.push(event);
|
|
10361
10827
|
if (translator.terminated) return;
|
|
@@ -10942,13 +11408,16 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
10942
11408
|
* account identity is the session's long-lived GitHub token (stable across
|
|
10943
11409
|
* Copilot-token refreshes, different per GitHub login); the conversation is
|
|
10944
11410
|
* the loop-stamped `sessionId`, falling back to the first message's id
|
|
10945
|
-
* when a hand-built request carries no session stamp
|
|
11411
|
+
* when a hand-built request carries no session stamp. Identity-free
|
|
11412
|
+
* requests have no replay scope; the model separates
|
|
10946
11413
|
* wire families. A call id captured in one scope is invisible to every
|
|
10947
11414
|
* other scope, so reused ids cannot leak reasoning across accounts,
|
|
10948
11415
|
* conversations, or models.
|
|
10949
11416
|
*/
|
|
10950
11417
|
replayScope(tokenKey, options) {
|
|
10951
|
-
|
|
11418
|
+
const conversation = options.sessionId !== void 0 ? `session:${String(options.sessionId)}` : options.messages[0]?.id !== void 0 ? `anchor:${String(options.messages[0].id)}` : void 0;
|
|
11419
|
+
if (conversation === void 0) return void 0;
|
|
11420
|
+
return `${tokenKey}\u0000${conversation}\u0000${options.model}`;
|
|
10952
11421
|
}
|
|
10953
11422
|
/**
|
|
10954
11423
|
* Store one response's completed reasoning items behind every call id it
|
|
@@ -10961,6 +11430,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
10961
11430
|
* per call.
|
|
10962
11431
|
*/
|
|
10963
11432
|
captureReasoning(scope, callIds, items) {
|
|
11433
|
+
if (scope === void 0) return;
|
|
10964
11434
|
let entries = this.replayByScope.get(scope);
|
|
10965
11435
|
if (entries === void 0) {
|
|
10966
11436
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -10996,6 +11466,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
10996
11466
|
* degradation, never an error.
|
|
10997
11467
|
*/
|
|
10998
11468
|
replayFor(scope, callId) {
|
|
11469
|
+
if (scope === void 0) return void 0;
|
|
10999
11470
|
const entries = this.replayByScope.get(scope);
|
|
11000
11471
|
const entry = entries?.get(callId);
|
|
11001
11472
|
if (entries === void 0 || entry === void 0) return void 0;
|
|
@@ -11151,8 +11622,8 @@ function object(value) {
|
|
|
11151
11622
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11152
11623
|
}
|
|
11153
11624
|
/** Resolve local references before removing definitions; reject cycles and remote references. */
|
|
11154
|
-
function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
11155
|
-
if (Array.isArray(value)) return value.map((item) => resolve(item, root, refs));
|
|
11625
|
+
function resolve$1(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
11626
|
+
if (Array.isArray(value)) return value.map((item) => resolve$1(item, root, refs));
|
|
11156
11627
|
if (!object(value)) return value;
|
|
11157
11628
|
if (typeof value.$ref === "string") {
|
|
11158
11629
|
const ref = value.$ref;
|
|
@@ -11164,7 +11635,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
|
11164
11635
|
}
|
|
11165
11636
|
if (!object(target)) throw new LlmError(`Antigravity tool schema reference is missing: ${ref}`, "INVALID_REQUEST");
|
|
11166
11637
|
const { $ref: _ref,...siblings } = value;
|
|
11167
|
-
return resolve({
|
|
11638
|
+
return resolve$1({
|
|
11168
11639
|
...target,
|
|
11169
11640
|
...siblings
|
|
11170
11641
|
}, root, new Set([...refs, ref]));
|
|
@@ -11174,7 +11645,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
|
11174
11645
|
"$id",
|
|
11175
11646
|
"$defs",
|
|
11176
11647
|
"definitions"
|
|
11177
|
-
].includes(key)).map(([key, item]) => [key, key === "properties" && object(item) ? Object.fromEntries(Object.entries(item).map(([name$1, property]) => [name$1, resolve(property, root, refs)])) : resolve(item, root, refs)]));
|
|
11648
|
+
].includes(key)).map(([key, item]) => [key, key === "properties" && object(item) ? Object.fromEntries(Object.entries(item).map(([name$1, property]) => [name$1, resolve$1(property, root, refs)])) : resolve$1(item, root, refs)]));
|
|
11178
11649
|
}
|
|
11179
11650
|
/** Numeric types widen to `number`: every integer is also a number. */
|
|
11180
11651
|
const NUMERIC = new Set(["integer", "number"]);
|
|
@@ -11260,7 +11731,7 @@ function custom(value) {
|
|
|
11260
11731
|
}
|
|
11261
11732
|
/** Detach and normalize one tool's root object without changing the registry schema. */
|
|
11262
11733
|
function antigravityToolParameters(parameters, legacy) {
|
|
11263
|
-
const resolved = resolve(parameters, parameters);
|
|
11734
|
+
const resolved = resolve$1(parameters, parameters);
|
|
11264
11735
|
const root = {
|
|
11265
11736
|
...resolved,
|
|
11266
11737
|
type: resolved.type ?? "object"
|
|
@@ -11276,9 +11747,11 @@ function antigravityToolParameters(parameters, legacy) {
|
|
|
11276
11747
|
* `FunctionResponse.response` field requires. That field maps to a singular
|
|
11277
11748
|
* protobuf `Struct`, so a bare array, string, number or boolean is rejected
|
|
11278
11749
|
* by the endpoint with "Proto field is not repeating, cannot start list".
|
|
11279
|
-
*
|
|
11750
|
+
* Non-array JSON objects use their own fields; every other JSON value is
|
|
11280
11751
|
* wrapped in the same "output" envelope the non-JSON path already uses --
|
|
11281
11752
|
* the shape other Cloud Code Assist clients send for every tool result.
|
|
11753
|
+
* Failed results carry top-level `isError: true`, overriding a payload field;
|
|
11754
|
+
* successful results retain their existing serialized shape.
|
|
11282
11755
|
*/
|
|
11283
11756
|
function toolResultValue(block) {
|
|
11284
11757
|
const text = block.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
@@ -11291,8 +11764,17 @@ function toolResultValue(block) {
|
|
|
11291
11764
|
...block.isError === true ? { isError: true } : {}
|
|
11292
11765
|
};
|
|
11293
11766
|
}
|
|
11294
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
11295
|
-
|
|
11767
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
11768
|
+
const value = parsed;
|
|
11769
|
+
return block.isError === true ? {
|
|
11770
|
+
...value,
|
|
11771
|
+
isError: true
|
|
11772
|
+
} : value;
|
|
11773
|
+
}
|
|
11774
|
+
return {
|
|
11775
|
+
output: parsed,
|
|
11776
|
+
...block.isError === true ? { isError: true } : {}
|
|
11777
|
+
};
|
|
11296
11778
|
}
|
|
11297
11779
|
/** Safely read per-block replay metadata emitted by this adapter. */
|
|
11298
11780
|
function replayBlocks(message, model) {
|
|
@@ -11342,7 +11824,8 @@ function toAntigravityContents(messages, model) {
|
|
|
11342
11824
|
response: toolResultValue({
|
|
11343
11825
|
type: "tool-result",
|
|
11344
11826
|
toolCallId: ToolCallId$1(id),
|
|
11345
|
-
content: message.content
|
|
11827
|
+
content: message.content,
|
|
11828
|
+
...message.isError === void 0 ? {} : { isError: message.isError }
|
|
11346
11829
|
})
|
|
11347
11830
|
} };
|
|
11348
11831
|
const previous$1 = out.at(-1);
|
|
@@ -11630,7 +12113,7 @@ async function* streamAntigravity(stream, onActivity) {
|
|
|
11630
12113
|
try {
|
|
11631
12114
|
parsed = JSON.parse(event.data);
|
|
11632
12115
|
} catch {
|
|
11633
|
-
throw new LlmError(
|
|
12116
|
+
throw new LlmError("malformed Antigravity SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
11634
12117
|
}
|
|
11635
12118
|
yield* translator.push(parsed);
|
|
11636
12119
|
if (translator.terminated) return;
|
|
@@ -11794,7 +12277,7 @@ async function discoverAntigravityAccount(accessToken, runtime = {}, fetchFn = h
|
|
|
11794
12277
|
projectId = projectIdOf(result.response?.cloudaicompanionProject);
|
|
11795
12278
|
break;
|
|
11796
12279
|
}
|
|
11797
|
-
await new Promise((resolve$
|
|
12280
|
+
await new Promise((resolve$2) => setTimeout(resolve$2, 1e3));
|
|
11798
12281
|
}
|
|
11799
12282
|
}
|
|
11800
12283
|
if (projectId === void 0) throw new Error("Antigravity account has no Cloud AI Companion project; open Antigravity and complete onboarding, then log in again");
|
|
@@ -12820,14 +13303,14 @@ function truncate(text, max = 60) {
|
|
|
12820
13303
|
/** Abort-aware sleep between two polls. */
|
|
12821
13304
|
function sleep(ms, signal) {
|
|
12822
13305
|
if (ms <= 0) return Promise.resolve();
|
|
12823
|
-
return new Promise((resolve$
|
|
13306
|
+
return new Promise((resolve$2, reject) => {
|
|
12824
13307
|
const onAbort = () => {
|
|
12825
13308
|
clearTimeout(timer);
|
|
12826
13309
|
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("video_generate: aborted"));
|
|
12827
13310
|
};
|
|
12828
13311
|
const timer = setTimeout(() => {
|
|
12829
13312
|
signal.removeEventListener("abort", onAbort);
|
|
12830
|
-
resolve$
|
|
13313
|
+
resolve$2();
|
|
12831
13314
|
}, ms);
|
|
12832
13315
|
if (signal.aborted) {
|
|
12833
13316
|
onAbort();
|
|
@@ -13141,6 +13624,7 @@ const Config = z.object({
|
|
|
13141
13624
|
]),
|
|
13142
13625
|
codexClientVersion: z.string(),
|
|
13143
13626
|
streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
13627
|
+
claudePromptCacheTtl: z.union(["5m", "1h"]).default(DEFAULT_PROMPT_CACHE_TTL),
|
|
13144
13628
|
rateLimit: z.object({
|
|
13145
13629
|
wait: z.boolean().default(true),
|
|
13146
13630
|
maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
|
|
@@ -13293,16 +13777,16 @@ const DEFAULT_MODELS = {
|
|
|
13293
13777
|
};
|
|
13294
13778
|
/** Validate and detach the model catalog for every provider. */
|
|
13295
13779
|
function resolveCatalog(models) {
|
|
13296
|
-
const resolve$
|
|
13780
|
+
const resolve$2 = (provider) => {
|
|
13297
13781
|
const configured = models?.[provider];
|
|
13298
13782
|
return validateModels(configured !== void 0 && configured.length > 0 ? configured : DEFAULT_MODELS[provider], `${name}: models.${provider}`);
|
|
13299
13783
|
};
|
|
13300
13784
|
return {
|
|
13301
|
-
codex: resolve$
|
|
13302
|
-
claude: resolve$
|
|
13303
|
-
grok: resolve$
|
|
13304
|
-
copilot: resolve$
|
|
13305
|
-
antigravity: resolve$
|
|
13785
|
+
codex: resolve$2("codex"),
|
|
13786
|
+
claude: resolve$2("claude"),
|
|
13787
|
+
grok: resolve$2("grok"),
|
|
13788
|
+
copilot: resolve$2("copilot"),
|
|
13789
|
+
antigravity: resolve$2("antigravity")
|
|
13306
13790
|
};
|
|
13307
13791
|
}
|
|
13308
13792
|
/** The display account of a stored session, for the status endpoint. */
|
|
@@ -13372,7 +13856,7 @@ var SubscriptionsAuthController = class {
|
|
|
13372
13856
|
* cannot be read off the flow manager.
|
|
13373
13857
|
*/
|
|
13374
13858
|
claims = /* @__PURE__ */ new Map();
|
|
13375
|
-
constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0, antigravityConfig = {}, clientVersions = {}) {
|
|
13859
|
+
constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0, antigravityConfig = {}, clientVersions = {}, resetRedemption) {
|
|
13376
13860
|
this.flows = flows;
|
|
13377
13861
|
this.deviceFlows = deviceFlows;
|
|
13378
13862
|
this.onAuthChanged = onAuthChanged;
|
|
@@ -13382,6 +13866,15 @@ var SubscriptionsAuthController = class {
|
|
|
13382
13866
|
this.poolUsage = poolUsage;
|
|
13383
13867
|
this.antigravityConfig = antigravityConfig;
|
|
13384
13868
|
this.clientVersions = clientVersions;
|
|
13869
|
+
this.resetRedemption = resetRedemption;
|
|
13870
|
+
}
|
|
13871
|
+
prepareReset(account, signal) {
|
|
13872
|
+
if (!this.resetRedemption) throw new BadRequest("Reset redemption unavailable");
|
|
13873
|
+
return this.resetRedemption.prepare(account, AbortSignal.any([signal, AbortSignal.timeout(2e4)]));
|
|
13874
|
+
}
|
|
13875
|
+
async consumeReset(account, ticket, signal) {
|
|
13876
|
+
if (!this.resetRedemption) throw new BadRequest("Reset redemption unavailable");
|
|
13877
|
+
await this.resetRedemption.redeem(account, ticket, AbortSignal.any([signal, AbortSignal.timeout(2e4)]));
|
|
13385
13878
|
}
|
|
13386
13879
|
usage(provider, account, signal, force = false) {
|
|
13387
13880
|
const fetcher = this.usageFetchers[provider];
|
|
@@ -13572,8 +14065,8 @@ function presentedVersion(cache, waitMs = STATUS_VERSION_WAIT_MS) {
|
|
|
13572
14065
|
return cache.current();
|
|
13573
14066
|
}
|
|
13574
14067
|
let timer;
|
|
13575
|
-
await Promise.race([cache.resolve(), new Promise((resolve$
|
|
13576
|
-
timer = setTimeout(resolve$
|
|
14068
|
+
await Promise.race([cache.resolve(), new Promise((resolve$2) => {
|
|
14069
|
+
timer = setTimeout(resolve$2, waitMs);
|
|
13577
14070
|
})]).finally(() => {
|
|
13578
14071
|
clearTimeout(timer);
|
|
13579
14072
|
});
|
|
@@ -13592,6 +14085,8 @@ function apply(ctx, config) {
|
|
|
13592
14085
|
const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
|
|
13593
14086
|
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
13594
14087
|
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0) throw new Error(`${name}: streamIdleTimeoutMs must be a positive finite number`);
|
|
14088
|
+
const claudePromptCacheTtl = config.claudePromptCacheTtl ?? DEFAULT_PROMPT_CACHE_TTL;
|
|
14089
|
+
if (claudePromptCacheTtl !== "5m" && claudePromptCacheTtl !== "1h") throw new Error(`${name}: claudePromptCacheTtl must be '5m' or '1h'`);
|
|
13595
14090
|
const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
|
|
13596
14091
|
const catalog = resolveCatalog(config.models);
|
|
13597
14092
|
const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
|
|
@@ -13659,7 +14154,22 @@ function apply(ctx, config) {
|
|
|
13659
14154
|
});
|
|
13660
14155
|
codexTokens = tokens;
|
|
13661
14156
|
accountTokens.set("codex", tokens);
|
|
13662
|
-
usageFetchers.codex = async (account, signal) =>
|
|
14157
|
+
usageFetchers.codex = async (account, signal) => {
|
|
14158
|
+
const session = await tokens.session(account);
|
|
14159
|
+
const usage = await fetchCodexUsage(session, hostFetch, signal);
|
|
14160
|
+
try {
|
|
14161
|
+
const resetCredits = await fetchCodexResetCredits(session, hostFetch, signal);
|
|
14162
|
+
return {
|
|
14163
|
+
...usage,
|
|
14164
|
+
resetCredits
|
|
14165
|
+
};
|
|
14166
|
+
} catch {
|
|
14167
|
+
return {
|
|
14168
|
+
...usage,
|
|
14169
|
+
resetCreditsError: "the usage-limit reset lookup did not answer"
|
|
14170
|
+
};
|
|
14171
|
+
}
|
|
14172
|
+
};
|
|
13663
14173
|
let adapter;
|
|
13664
14174
|
adapter = new CodexAdapter({
|
|
13665
14175
|
...config.codexClientVersion === void 0 ? {} : { clientVersion: config.codexClientVersion },
|
|
@@ -13710,6 +14220,7 @@ function apply(ctx, config) {
|
|
|
13710
14220
|
catalogStore: catalogStore("claude"),
|
|
13711
14221
|
defaultEffortOf: (model) => defaultEffortOf("claude", model),
|
|
13712
14222
|
resolveCliVersion: () => claudeVersion.resolve(),
|
|
14223
|
+
promptCacheTtl: claudePromptCacheTtl,
|
|
13713
14224
|
pool: () => poolAdapter
|
|
13714
14225
|
});
|
|
13715
14226
|
adapters.set("claude", adapter);
|
|
@@ -13817,8 +14328,8 @@ function apply(ctx, config) {
|
|
|
13817
14328
|
const fetcherFor = (provider, account) => {
|
|
13818
14329
|
switch (provider) {
|
|
13819
14330
|
case "codex": {
|
|
13820
|
-
const
|
|
13821
|
-
return
|
|
14331
|
+
const fetcher = usageFetchers.codex;
|
|
14332
|
+
return fetcher === void 0 ? void 0 : () => fetcher(account, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
13822
14333
|
}
|
|
13823
14334
|
case "claude": {
|
|
13824
14335
|
const tokens = claudeTokens;
|
|
@@ -13993,13 +14504,20 @@ function apply(ctx, config) {
|
|
|
13993
14504
|
cursorHandle = credentialsCtx.llm.registerAdapter(["cursor-subscription"], cursorAdapter);
|
|
13994
14505
|
});
|
|
13995
14506
|
const externalUsage = new ExternalUsageController(async (name$1) => resolveExternalCredential?.(name$1), hostFetch);
|
|
14507
|
+
const resetRedemption = new ResetRedemption(async (account, signal) => {
|
|
14508
|
+
if (!usageFetchers.codex) throw new BadRequest("Codex unavailable");
|
|
14509
|
+
return usageFetchers.codex(account, signal);
|
|
14510
|
+
}, async (account, creditId, requestId, signal) => {
|
|
14511
|
+
if (!codexTokens) throw new BadRequest("Codex unavailable");
|
|
14512
|
+
await consumeCodexResetCredit(await codexTokens.session(account), creditId, requestId, hostFetch, signal);
|
|
14513
|
+
}, (account) => poolUsage?.invalidate("codex", account), async (account) => resolveAccountKey("codex", account));
|
|
13996
14514
|
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage, config.antigravity, {
|
|
13997
14515
|
...providers.includes("codex") ? { codex: config.codexClientVersion === void 0 ? presentedVersion(codexVersion) : async () => ({
|
|
13998
14516
|
version: config.codexClientVersion,
|
|
13999
14517
|
source: "config"
|
|
14000
14518
|
}) } : {},
|
|
14001
14519
|
...providers.includes("claude") ? { claude: presentedVersion(claudeVersion) } : {}
|
|
14002
|
-
}), speed, modelDefaults, {
|
|
14520
|
+
}, resetRedemption), speed, modelDefaults, {
|
|
14003
14521
|
async get(provider, force) {
|
|
14004
14522
|
await loadModelDefaults();
|
|
14005
14523
|
const adapter = adapters.get(provider);
|