dsh-subscriptions 0.1.3 → 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 +64 -0
- package/lib/auth/rpc.d.ts +3 -1
- package/lib/auth/rpc.js +10 -1
- 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.js +6 -5
- package/lib/client/SubscriptionsSection.d.ts +22 -0
- package/lib/client/SubscriptionsSection.js +128 -38
- package/lib/client/UsageBadgeDisplaySetting.js +4 -8
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +22 -0
- package/lib/client/locales.d.ts +58 -0
- package/lib/client/locales.js +58 -0
- 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.js +937 -310
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +9 -0
- package/lib/index.js +1099 -608
- 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/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);
|
|
@@ -1367,6 +1442,8 @@ const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
|
|
|
1367
1442
|
"cursorSetSettings",
|
|
1368
1443
|
"image",
|
|
1369
1444
|
"video",
|
|
1445
|
+
"prepareReset",
|
|
1446
|
+
"consumeReset",
|
|
1370
1447
|
"speed",
|
|
1371
1448
|
"setSpeed",
|
|
1372
1449
|
"modelDefaults",
|
|
@@ -1646,6 +1723,13 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
|
|
|
1646
1723
|
if (payload === null || typeof payload !== "object") throw new BadRequest("payload must be an object");
|
|
1647
1724
|
await cursor.setSettings(payload.settings);
|
|
1648
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 });
|
|
1649
1733
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
1650
1734
|
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
1651
1735
|
case "speed": return ok(await speed.speed(readSessionId(payload)));
|
|
@@ -1716,10 +1800,10 @@ function tokenExpiry(token, now) {
|
|
|
1716
1800
|
}
|
|
1717
1801
|
function abortableDelay$2(ms, signal) {
|
|
1718
1802
|
if (signal.aborted) return Promise.reject(signal.reason);
|
|
1719
|
-
return new Promise((resolve$
|
|
1803
|
+
return new Promise((resolve$2, reject) => {
|
|
1720
1804
|
const timer = setTimeout(() => {
|
|
1721
1805
|
signal.removeEventListener("abort", aborted);
|
|
1722
|
-
resolve$
|
|
1806
|
+
resolve$2();
|
|
1723
1807
|
}, ms);
|
|
1724
1808
|
function aborted() {
|
|
1725
1809
|
clearTimeout(timer);
|
|
@@ -2207,7 +2291,7 @@ async function nativeFetch(url, init = {}) {
|
|
|
2207
2291
|
const client = target.protocol === "https:" ? https : http;
|
|
2208
2292
|
const method = (init.method ?? "GET").toUpperCase();
|
|
2209
2293
|
const body = init.body === void 0 ? void 0 : isBytes(init.body) ? Buffer.from(init.body) : String(init.body);
|
|
2210
|
-
const request = (u$1, redirect) => new Promise((resolve$
|
|
2294
|
+
const request = (u$1, redirect) => new Promise((resolve$2, reject) => {
|
|
2211
2295
|
const req = client.request(u$1, {
|
|
2212
2296
|
method,
|
|
2213
2297
|
headers: sanitizeHeaders(init.headers),
|
|
@@ -2224,10 +2308,10 @@ async function nativeFetch(url, init = {}) {
|
|
|
2224
2308
|
}
|
|
2225
2309
|
const location = res.headers.location;
|
|
2226
2310
|
if ((redirect ?? "follow") === "follow" && location && status >= 300 && status < 400) {
|
|
2227
|
-
resolve$
|
|
2311
|
+
resolve$2({ redirect: new URL(location, u$1) });
|
|
2228
2312
|
return;
|
|
2229
2313
|
}
|
|
2230
|
-
resolve$
|
|
2314
|
+
resolve$2({ response: makeResponse(status, res, data) });
|
|
2231
2315
|
});
|
|
2232
2316
|
});
|
|
2233
2317
|
req.on("error", reject);
|
|
@@ -2385,12 +2469,12 @@ function isSuccessfulAgentResponse(status, contentType) {
|
|
|
2385
2469
|
function abortableDelay$1(ms, signal) {
|
|
2386
2470
|
if (signal.aborted) return Promise.reject(signal.reason ?? /* @__PURE__ */ new Error("Cursor retry aborted"));
|
|
2387
2471
|
if (ms === 0) return Promise.resolve();
|
|
2388
|
-
return new Promise((resolve$
|
|
2472
|
+
return new Promise((resolve$2, reject) => {
|
|
2389
2473
|
const timer = setTimeout(done, ms);
|
|
2390
2474
|
timer.unref?.();
|
|
2391
2475
|
function done() {
|
|
2392
2476
|
signal.removeEventListener("abort", aborted);
|
|
2393
|
-
resolve$
|
|
2477
|
+
resolve$2();
|
|
2394
2478
|
}
|
|
2395
2479
|
function aborted() {
|
|
2396
2480
|
clearTimeout(timer);
|
|
@@ -3170,8 +3254,8 @@ var ConnectFrameReader = class {
|
|
|
3170
3254
|
if (this.error !== void 0) throw this.error;
|
|
3171
3255
|
return;
|
|
3172
3256
|
}
|
|
3173
|
-
return new Promise((resolve$
|
|
3174
|
-
resolve: resolve$
|
|
3257
|
+
return new Promise((resolve$2, reject) => this.waiters.push({
|
|
3258
|
+
resolve: resolve$2,
|
|
3175
3259
|
reject
|
|
3176
3260
|
}));
|
|
3177
3261
|
}
|
|
@@ -3209,8 +3293,8 @@ var AgentRun = class {
|
|
|
3209
3293
|
authorization: `Bearer ${this.accessToken}`,
|
|
3210
3294
|
...AGENT_HEADERS
|
|
3211
3295
|
};
|
|
3212
|
-
this.responsePromise = new Promise((resolve$
|
|
3213
|
-
this.resolveResponse = resolve$
|
|
3296
|
+
this.responsePromise = new Promise((resolve$2, reject) => {
|
|
3297
|
+
this.resolveResponse = resolve$2;
|
|
3214
3298
|
this.rejectResponse = reject;
|
|
3215
3299
|
});
|
|
3216
3300
|
this.responsePromise.catch(() => {});
|
|
@@ -3239,10 +3323,10 @@ var AgentRun = class {
|
|
|
3239
3323
|
});
|
|
3240
3324
|
}
|
|
3241
3325
|
#resolveResponse(status) {
|
|
3242
|
-
const resolve$
|
|
3326
|
+
const resolve$2 = this.resolveResponse;
|
|
3243
3327
|
this.resolveResponse = void 0;
|
|
3244
3328
|
this.rejectResponse = void 0;
|
|
3245
|
-
resolve$
|
|
3329
|
+
resolve$2?.(status);
|
|
3246
3330
|
}
|
|
3247
3331
|
#rejectResponse(error) {
|
|
3248
3332
|
const reject = this.rejectResponse;
|
|
@@ -3253,10 +3337,10 @@ var AgentRun = class {
|
|
|
3253
3337
|
waitForResponse(timeoutMs) {
|
|
3254
3338
|
if (this.responsePromise === void 0) throw new Error("Cursor AgentRun has not started");
|
|
3255
3339
|
if (timeoutMs === void 0) return this.responsePromise;
|
|
3256
|
-
return new Promise((resolve$
|
|
3340
|
+
return new Promise((resolve$2, reject) => {
|
|
3257
3341
|
const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Cursor HTTP response timeout")), timeoutMs);
|
|
3258
3342
|
timer.unref?.();
|
|
3259
|
-
this.responsePromise.then(resolve$
|
|
3343
|
+
this.responsePromise.then(resolve$2, reject).finally(() => clearTimeout(timer));
|
|
3260
3344
|
});
|
|
3261
3345
|
}
|
|
3262
3346
|
write(payload) {
|
|
@@ -4798,31 +4882,6 @@ function earliestReset(...candidates) {
|
|
|
4798
4882
|
function waitFromReset(instant, now) {
|
|
4799
4883
|
return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
|
|
4800
4884
|
}
|
|
4801
|
-
/** Header names worth showing when a 429 disclosed no reset this code recognizes. */
|
|
4802
|
-
const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
|
|
4803
|
-
/**
|
|
4804
|
-
* Render the rate-limit-shaped headers and the head of the body of a 429 whose
|
|
4805
|
-
* reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
|
|
4806
|
-
* how an unrecognized provider field gets named from live traffic instead of
|
|
4807
|
-
* being guessed at.
|
|
4808
|
-
*
|
|
4809
|
-
* It is also where the per-bucket rollover snapshots land by design — no reader
|
|
4810
|
-
* parks a turn on one, because on a 429 they cannot say which bucket refused —
|
|
4811
|
-
* so the operator still sees what the provider disclosed.
|
|
4812
|
-
* @param response - the failed response.
|
|
4813
|
-
* @param body - the complete response body.
|
|
4814
|
-
* @returns a one-line diagnostic.
|
|
4815
|
-
*/
|
|
4816
|
-
function rateLimitDiagnostics(response, body) {
|
|
4817
|
-
const headers = [];
|
|
4818
|
-
response.headers.forEach((value, key) => {
|
|
4819
|
-
if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
|
|
4820
|
-
});
|
|
4821
|
-
headers.sort();
|
|
4822
|
-
const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
|
|
4823
|
-
const head = body.slice(0, 200);
|
|
4824
|
-
return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
|
|
4825
|
-
}
|
|
4826
4885
|
/**
|
|
4827
4886
|
* The retry shape every subscription route starts from: Claude Code's own SDK
|
|
4828
4887
|
* numbers — ten retries after the first attempt, exponential backoff from 1s
|
|
@@ -4923,6 +4982,17 @@ function validateModels(models, label) {
|
|
|
4923
4982
|
});
|
|
4924
4983
|
}
|
|
4925
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
|
+
/**
|
|
4926
4996
|
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
4927
4997
|
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
4928
4998
|
* the `providerRetryAfterMs` the retry plugin waits out.
|
|
@@ -4943,7 +5013,6 @@ async function httpLlmError(response, label, options = {}) {
|
|
|
4943
5013
|
body = await response.text();
|
|
4944
5014
|
} catch {}
|
|
4945
5015
|
const shown = body.slice(0, 500);
|
|
4946
|
-
const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
|
|
4947
5016
|
let code;
|
|
4948
5017
|
if (response.status === 401 || response.status === 403) code = "AUTH";
|
|
4949
5018
|
else if (response.status === 429) code = "RATE_LIMIT";
|
|
@@ -4952,10 +5021,11 @@ async function httpLlmError(response, label, options = {}) {
|
|
|
4952
5021
|
else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
|
|
4953
5022
|
else if (response.status >= 500) code = "SERVER";
|
|
4954
5023
|
else code = `HTTP_${String(response.status)}`;
|
|
5024
|
+
const message = providerDiagnostic(`${label} error (HTTP ${String(response.status)}, ${code})`, body.length > 0);
|
|
4955
5025
|
const now = Date.now();
|
|
4956
5026
|
const rateLimited = response.status === 429;
|
|
4957
5027
|
const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? googleQuotaReset(body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
|
|
4958
|
-
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));
|
|
4959
5029
|
return new LlmError(message, code, {
|
|
4960
5030
|
status: response.status,
|
|
4961
5031
|
...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
|
|
@@ -5079,19 +5149,17 @@ var OAuthEndpointError = class extends Error {
|
|
|
5079
5149
|
*/
|
|
5080
5150
|
async function oauthEndpointError(response, label) {
|
|
5081
5151
|
let oauthCode;
|
|
5082
|
-
let
|
|
5152
|
+
let body = "";
|
|
5083
5153
|
try {
|
|
5084
|
-
|
|
5154
|
+
body = await response.text();
|
|
5155
|
+
const parsed = JSON.parse(body);
|
|
5085
5156
|
if (typeof parsed.error === "string") oauthCode = parsed.error;
|
|
5086
5157
|
else if (typeof parsed.error === "object" && parsed.error !== null) {
|
|
5087
5158
|
const nested = parsed.error;
|
|
5088
5159
|
if (typeof nested.code === "string" && nested.code.length > 0) oauthCode = nested.code;
|
|
5089
|
-
if (typeof nested.message === "string") detail = nested.message;
|
|
5090
5160
|
}
|
|
5091
|
-
if (typeof parsed.error_description === "string") detail = parsed.error_description;
|
|
5092
|
-
if (detail.length === 0) detail = oauthCode ?? "";
|
|
5093
5161
|
} catch {}
|
|
5094
|
-
return new OAuthEndpointError(
|
|
5162
|
+
return new OAuthEndpointError(providerDiagnostic(`${label} token endpoint error (HTTP ${String(response.status)})`, body.length > 0), response.status, oauthCode, parseRetryAfterMs(response));
|
|
5095
5163
|
}
|
|
5096
5164
|
/**
|
|
5097
5165
|
* Per-provider session freshness: loads the stored session, refreshes
|
|
@@ -5170,9 +5238,9 @@ const DISCOVERY_TIMEOUT_MS = 1e4;
|
|
|
5170
5238
|
*/
|
|
5171
5239
|
function withTimeout(work, timeoutMs) {
|
|
5172
5240
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
5173
|
-
const aborted = new Promise((resolve$
|
|
5174
|
-
if (signal.aborted) resolve$
|
|
5175
|
-
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 });
|
|
5176
5244
|
});
|
|
5177
5245
|
return Promise.race([work(signal).then((value) => signal.aborted ? void 0 : value, (error) => {
|
|
5178
5246
|
if (signal.aborted) return void 0;
|
|
@@ -5531,446 +5599,144 @@ var AccountTokenManager = class {
|
|
|
5531
5599
|
};
|
|
5532
5600
|
|
|
5533
5601
|
//#endregion
|
|
5534
|
-
//#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
|
|
5535
5611
|
/**
|
|
5536
|
-
*
|
|
5537
|
-
* @
|
|
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.
|
|
5538
5616
|
*/
|
|
5539
|
-
function
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
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();
|
|
5567
5652
|
}
|
|
5568
|
-
if (raw.defaultEffort !== void 0 && (typeof raw.defaultEffort !== "string" || !seen.has(raw.defaultEffort))) return void 0;
|
|
5569
|
-
return {
|
|
5570
|
-
efforts,
|
|
5571
|
-
...raw.defaultEffort === void 0 ? {} : { defaultEffort: ReasoningEffortId(raw.defaultEffort) }
|
|
5572
|
-
};
|
|
5573
|
-
}
|
|
5574
|
-
/** Validate one persisted model, or undefined when malformed. */
|
|
5575
|
-
function sanitizeModel(value) {
|
|
5576
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
5577
|
-
const raw = value;
|
|
5578
|
-
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;
|
|
5579
|
-
const reasoning = raw.reasoning === void 0 ? void 0 : sanitizeReasoning(raw.reasoning);
|
|
5580
|
-
if (raw.reasoning !== void 0 && reasoning === void 0) return void 0;
|
|
5581
|
-
const thinkingType = raw.thinkingType;
|
|
5582
|
-
if (thinkingType !== void 0 && thinkingType !== "enabled" && thinkingType !== "adaptive") return void 0;
|
|
5583
|
-
const fastTier = raw.fastTier;
|
|
5584
|
-
if (fastTier !== void 0 && typeof fastTier !== "boolean") return void 0;
|
|
5585
|
-
const copilotWire = raw.copilotWire;
|
|
5586
|
-
if (copilotWire !== void 0 && copilotWire !== "chat-completions" && copilotWire !== "responses") return;
|
|
5587
|
-
const copilotResponses = raw.copilotResponses;
|
|
5588
|
-
if (copilotResponses !== void 0 && typeof copilotResponses !== "boolean") return void 0;
|
|
5589
|
-
const inputModalities = raw.inputModalities;
|
|
5590
|
-
if (inputModalities !== void 0 && (!Array.isArray(inputModalities) || inputModalities.length === 0 || inputModalities.some((modality) => modality !== "text" && modality !== "image"))) return void 0;
|
|
5591
|
-
return {
|
|
5592
|
-
id: raw.id,
|
|
5593
|
-
name: raw.name,
|
|
5594
|
-
...raw.description === void 0 ? {} : { description: raw.description },
|
|
5595
|
-
...raw.contextWindow === void 0 ? {} : { contextWindow: raw.contextWindow },
|
|
5596
|
-
...raw.maxContextWindow === void 0 ? {} : { maxContextWindow: raw.maxContextWindow },
|
|
5597
|
-
...raw.maxOutputTokens === void 0 ? {} : { maxOutputTokens: raw.maxOutputTokens },
|
|
5598
|
-
...raw.priority === void 0 ? {} : { priority: raw.priority },
|
|
5599
|
-
...reasoning === void 0 ? {} : { reasoning },
|
|
5600
|
-
...thinkingType === void 0 ? {} : { thinkingType },
|
|
5601
|
-
...fastTier === void 0 ? {} : { fastTier },
|
|
5602
|
-
...copilotWire === void 0 ? {} : { copilotWire },
|
|
5603
|
-
...copilotResponses === void 0 ? {} : { copilotResponses },
|
|
5604
|
-
...inputModalities === void 0 ? {} : { inputModalities: [...inputModalities] }
|
|
5605
|
-
};
|
|
5606
5653
|
}
|
|
5654
|
+
|
|
5655
|
+
//#endregion
|
|
5656
|
+
//#region src/translate/anthropic.ts
|
|
5607
5657
|
/**
|
|
5608
|
-
*
|
|
5609
|
-
*
|
|
5610
|
-
*
|
|
5611
|
-
* @param value - the raw per-provider file entry.
|
|
5612
|
-
* @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.
|
|
5613
5661
|
*/
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5618
|
-
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
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
|
+
});
|
|
5626
5715
|
}
|
|
5627
|
-
return
|
|
5628
|
-
at: raw.at,
|
|
5629
|
-
models
|
|
5630
|
-
};
|
|
5716
|
+
return content;
|
|
5631
5717
|
}
|
|
5632
|
-
/**
|
|
5633
|
-
|
|
5634
|
-
let text;
|
|
5718
|
+
/** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
|
|
5719
|
+
function parseToolInput(raw) {
|
|
5635
5720
|
try {
|
|
5636
|
-
|
|
5637
|
-
|
|
5721
|
+
const parsed = JSON.parse(raw);
|
|
5722
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
5638
5723
|
return {};
|
|
5639
|
-
}
|
|
5640
|
-
try {
|
|
5641
|
-
const parsed = JSON.parse(text);
|
|
5642
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
5643
|
-
return parsed;
|
|
5644
5724
|
} catch {
|
|
5645
5725
|
return {};
|
|
5646
5726
|
}
|
|
5647
5727
|
}
|
|
5648
|
-
/** Persist the whole file atomically (tmp file + rename). */
|
|
5649
|
-
async function writeCatalogFile(store, path) {
|
|
5650
|
-
await mkdir(dirname(path), { recursive: true });
|
|
5651
|
-
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
5652
|
-
try {
|
|
5653
|
-
await writeFile(tmp, JSON.stringify(store, null, 2));
|
|
5654
|
-
await rename(tmp, path);
|
|
5655
|
-
} catch (error) {
|
|
5656
|
-
await rm(tmp, { force: true });
|
|
5657
|
-
throw error;
|
|
5658
|
-
}
|
|
5659
|
-
}
|
|
5660
5728
|
/**
|
|
5661
|
-
*
|
|
5662
|
-
*
|
|
5663
|
-
*
|
|
5664
|
-
*
|
|
5665
|
-
*
|
|
5666
|
-
*
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
},
|
|
5673
|
-
async save(snapshot) {
|
|
5674
|
-
const store = await readCatalogFile(path);
|
|
5675
|
-
store[provider] = snapshot;
|
|
5676
|
-
await writeCatalogFile(store, path);
|
|
5677
|
-
},
|
|
5678
|
-
async clear() {
|
|
5679
|
-
const store = await readCatalogFile(path);
|
|
5680
|
-
if (store[provider] === void 0) return;
|
|
5681
|
-
delete store[provider];
|
|
5682
|
-
await writeCatalogFile(store, path);
|
|
5683
|
-
}
|
|
5684
|
-
};
|
|
5685
|
-
}
|
|
5686
|
-
/**
|
|
5687
|
-
* The durable half of one NON-default account's catalog cache. The provider
|
|
5688
|
-
* entry belongs to the default account; every other account keeps its own
|
|
5689
|
-
* snapshot under `accounts[provider][account]`, so a working secondary login
|
|
5690
|
-
* survives restarts and network failures even when the default login is dead.
|
|
5691
|
-
* @param provider - the provider route.
|
|
5692
|
-
* @param account - the canonical account key.
|
|
5693
|
-
* @param path - store file path; defaults to {@link modelsFilePath}.
|
|
5694
|
-
* @returns the persistence hooks for {@link ModelCatalogCache}.
|
|
5695
|
-
*/
|
|
5696
|
-
function accountCatalogStore(provider, account, path = modelsFilePath()) {
|
|
5697
|
-
return {
|
|
5698
|
-
async load() {
|
|
5699
|
-
const section = accountSection(await readCatalogFile(path), provider);
|
|
5700
|
-
return section !== void 0 && Object.hasOwn(section, account) ? sanitizeSnapshot(section[account]) : void 0;
|
|
5701
|
-
},
|
|
5702
|
-
async save(snapshot) {
|
|
5703
|
-
const store = await readCatalogFile(path);
|
|
5704
|
-
const accounts = typeof store.accounts === "object" && store.accounts !== null && !Array.isArray(store.accounts) ? store.accounts : {};
|
|
5705
|
-
const section = {
|
|
5706
|
-
...accountSection(store, provider) ?? {},
|
|
5707
|
-
[account]: snapshot
|
|
5708
|
-
};
|
|
5709
|
-
store.accounts = {
|
|
5710
|
-
...accounts,
|
|
5711
|
-
[provider]: section
|
|
5712
|
-
};
|
|
5713
|
-
await writeCatalogFile(store, path);
|
|
5714
|
-
},
|
|
5715
|
-
async clear() {
|
|
5716
|
-
const store = await readCatalogFile(path);
|
|
5717
|
-
const section = accountSection(store, provider);
|
|
5718
|
-
if (section === void 0 || !Object.hasOwn(section, account)) return;
|
|
5719
|
-
const rest = { ...section };
|
|
5720
|
-
delete rest[account];
|
|
5721
|
-
store.accounts = {
|
|
5722
|
-
...store.accounts,
|
|
5723
|
-
[provider]: rest
|
|
5724
|
-
};
|
|
5725
|
-
await writeCatalogFile(store, path);
|
|
5726
|
-
}
|
|
5727
|
-
};
|
|
5728
|
-
}
|
|
5729
|
-
|
|
5730
|
-
//#endregion
|
|
5731
|
-
//#region src/translate/resolved.ts
|
|
5732
|
-
/**
|
|
5733
|
-
* Wires with text-only tool outputs receive images in a following user turn.
|
|
5734
|
-
* Defer that turn until all consecutive user messages have been processed:
|
|
5735
|
-
* parallel tool results can arrive in separate harness messages, and a user
|
|
5736
|
-
* image message must not interrupt their tool-call/output pairing.
|
|
5737
|
-
*/
|
|
5738
|
-
function withToolResultImages(messages) {
|
|
5739
|
-
const out = [];
|
|
5740
|
-
let images = [];
|
|
5741
|
-
const flush = () => {
|
|
5742
|
-
if (images.length > 0) out.push({
|
|
5743
|
-
role: "user",
|
|
5744
|
-
content: images
|
|
5745
|
-
});
|
|
5746
|
-
images = [];
|
|
5747
|
-
};
|
|
5748
|
-
for (const message of messages) {
|
|
5749
|
-
if (message.role === "assistant") flush();
|
|
5750
|
-
out.push(message);
|
|
5751
|
-
if (message.role === "tool") {
|
|
5752
|
-
const parts = message.content.filter((part) => part.type === "image" && "dataBase64" in part);
|
|
5753
|
-
if (parts.length > 0) images.push({
|
|
5754
|
-
type: "text",
|
|
5755
|
-
text: `Images from tool result ${String(message.toolCallId)}:`
|
|
5756
|
-
}, ...parts);
|
|
5757
|
-
}
|
|
5758
|
-
for (const block of message.content) {
|
|
5759
|
-
if (block.type !== "tool-result") continue;
|
|
5760
|
-
const parts = block.content.filter((part) => part.type === "image" && "dataBase64" in part);
|
|
5761
|
-
if (parts.length > 0) images.push({
|
|
5762
|
-
type: "text",
|
|
5763
|
-
text: `Images from tool result ${String(block.toolCallId)}:`
|
|
5764
|
-
}, ...parts);
|
|
5765
|
-
}
|
|
5766
|
-
}
|
|
5767
|
-
flush();
|
|
5768
|
-
return out;
|
|
5769
|
-
}
|
|
5770
|
-
/**
|
|
5771
|
-
* The request projection for one oversized image, or undefined when it fits.
|
|
5772
|
-
* Hosts before DSH 0.1.7 read a pixel budget (`maxPixels`), later ones the
|
|
5773
|
-
* target edges (`width`/`height`); each validates only its own fields, so
|
|
5774
|
-
* one projection carries both.
|
|
5775
|
-
*/
|
|
5776
|
-
function imageRequestTarget(ref, limit) {
|
|
5777
|
-
const longEdge = Math.max(ref.width, ref.height);
|
|
5778
|
-
if (!(longEdge > limit.maxEdge)) return void 0;
|
|
5779
|
-
const short = (edge) => Math.max(1, Math.floor(edge * limit.maxEdge / longEdge));
|
|
5780
|
-
const width = ref.width >= ref.height ? limit.maxEdge : short(ref.width);
|
|
5781
|
-
const height = ref.width >= ref.height ? short(ref.height) : limit.maxEdge;
|
|
5782
|
-
return {
|
|
5783
|
-
width,
|
|
5784
|
-
height,
|
|
5785
|
-
maxPixels: width * height,
|
|
5786
|
-
maxBytes: limit.maxBytes
|
|
5787
|
-
};
|
|
5788
|
-
}
|
|
5789
|
-
/**
|
|
5790
|
-
* Resolve every ImageBlock's attachment reference to inline base64 bytes.
|
|
5791
|
-
* Messages without images pass through unchanged. A request carrying an image
|
|
5792
|
-
* with no attachment service available fails loudly rather than silently
|
|
5793
|
-
* dropping the image.
|
|
5794
|
-
* @param messages - the request's conversation messages.
|
|
5795
|
-
* @param attachments - the deployment's attachment service, when mounted.
|
|
5796
|
-
* @param signal - cancellation for the storage reads.
|
|
5797
|
-
* @param limit - the route's outgoing image cap; oversized images are sent
|
|
5798
|
-
* downscaled while the stored attachment and history stay untouched.
|
|
5799
|
-
* @returns the same messages with image blocks resolved for the translators.
|
|
5800
|
-
*/
|
|
5801
|
-
async function resolveImages(messages, attachments, signal, limit) {
|
|
5802
|
-
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");
|
|
5803
|
-
const readForRequest = async (ref) => {
|
|
5804
|
-
if (attachments === void 0) throw new Error("attachments service is unavailable");
|
|
5805
|
-
const target = limit === void 0 ? void 0 : imageRequestTarget(ref, limit);
|
|
5806
|
-
if (target !== void 0) try {
|
|
5807
|
-
const version = await attachments.readImageRequest(ref, target, signal);
|
|
5808
|
-
return {
|
|
5809
|
-
data: version.data,
|
|
5810
|
-
mediaType: version.mediaType,
|
|
5811
|
-
ref: version.attachment
|
|
5812
|
-
};
|
|
5813
|
-
} catch (error) {
|
|
5814
|
-
if (signal?.aborted) throw error;
|
|
5815
|
-
}
|
|
5816
|
-
const stored = await attachments.readImage(ref, signal);
|
|
5817
|
-
return {
|
|
5818
|
-
data: stored.data,
|
|
5819
|
-
mediaType: stored.ref.mediaType,
|
|
5820
|
-
ref: stored.ref
|
|
5821
|
-
};
|
|
5822
|
-
};
|
|
5823
|
-
const resolveBlock = async (block) => {
|
|
5824
|
-
if (block.type !== "image") return [block];
|
|
5825
|
-
const { data, mediaType: sentType, ref } = await readForRequest(block.attachment);
|
|
5826
|
-
const { attachmentId, mediaType, bytes, width, height, name: name$1 } = ref;
|
|
5827
|
-
return [{
|
|
5828
|
-
type: "image",
|
|
5829
|
-
mediaType: sentType,
|
|
5830
|
-
dataBase64: Buffer.from(data).toString("base64")
|
|
5831
|
-
}, {
|
|
5832
|
-
type: "text",
|
|
5833
|
-
text: `Image reference (for image_generate.referenceImages): ${JSON.stringify({
|
|
5834
|
-
attachmentId,
|
|
5835
|
-
mediaType,
|
|
5836
|
-
bytes,
|
|
5837
|
-
width,
|
|
5838
|
-
height,
|
|
5839
|
-
...name$1 === void 0 ? {} : { name: name$1 }
|
|
5840
|
-
})}`
|
|
5841
|
-
}];
|
|
5842
|
-
};
|
|
5843
|
-
return Promise.all(messages.map(async (message) => ({
|
|
5844
|
-
role: message.role,
|
|
5845
|
-
...message.source === void 0 ? {} : { source: message.source },
|
|
5846
|
-
...message.role === "tool" ? {
|
|
5847
|
-
toolCallId: message.toolCallId,
|
|
5848
|
-
isError: message.isError
|
|
5849
|
-
} : {},
|
|
5850
|
-
content: (await Promise.all(message.content.map(resolveBlock))).flat()
|
|
5851
|
-
})));
|
|
5852
|
-
}
|
|
5853
|
-
|
|
5854
|
-
//#endregion
|
|
5855
|
-
//#region src/compat.ts
|
|
5856
|
-
/** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
|
|
5857
|
-
const ToolCallId$1 = (() => {
|
|
5858
|
-
const exports = llm;
|
|
5859
|
-
return exports["ToolCallId"] ?? exports["CallId"];
|
|
5860
|
-
})();
|
|
5861
|
-
|
|
5862
|
-
//#endregion
|
|
5863
|
-
//#region src/translate/sse.ts
|
|
5864
|
-
/**
|
|
5865
|
-
* Decode an SSE byte stream into events.
|
|
5866
|
-
* @param stream - raw response bytes; reads may split anywhere, including mid-UTF-8 sequence.
|
|
5867
|
-
* @param onActivity - called on every received chunk and comment line; drives the idle watchdog.
|
|
5868
|
-
* @returns events in arrival order.
|
|
5869
|
-
*/
|
|
5870
|
-
async function* parseSse(stream, onActivity) {
|
|
5871
|
-
const reader = stream.getReader();
|
|
5872
|
-
const decoder = new TextDecoder();
|
|
5873
|
-
let pending = "";
|
|
5874
|
-
let dataLines = [];
|
|
5875
|
-
let eventName;
|
|
5876
|
-
try {
|
|
5877
|
-
while (true) {
|
|
5878
|
-
const { done, value } = await reader.read();
|
|
5879
|
-
if (done) return;
|
|
5880
|
-
onActivity?.();
|
|
5881
|
-
pending += decoder.decode(value, { stream: true });
|
|
5882
|
-
let newline = pending.indexOf("\n");
|
|
5883
|
-
while (newline >= 0) {
|
|
5884
|
-
let line = pending.slice(0, newline);
|
|
5885
|
-
pending = pending.slice(newline + 1);
|
|
5886
|
-
newline = pending.indexOf("\n");
|
|
5887
|
-
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
5888
|
-
if (line.length === 0) {
|
|
5889
|
-
if (dataLines.length > 0) yield {
|
|
5890
|
-
data: dataLines.join("\n"),
|
|
5891
|
-
...eventName === void 0 ? {} : { event: eventName }
|
|
5892
|
-
};
|
|
5893
|
-
dataLines = [];
|
|
5894
|
-
eventName = void 0;
|
|
5895
|
-
} else if (line.startsWith(":")) onActivity?.();
|
|
5896
|
-
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
5897
|
-
else if (line.startsWith("event:")) eventName = line.slice(6).replace(/^ /, "");
|
|
5898
|
-
}
|
|
5899
|
-
}
|
|
5900
|
-
} finally {
|
|
5901
|
-
reader.releaseLock();
|
|
5902
|
-
}
|
|
5903
|
-
}
|
|
5904
|
-
|
|
5905
|
-
//#endregion
|
|
5906
|
-
//#region src/translate/anthropic.ts
|
|
5907
|
-
/**
|
|
5908
|
-
* The Claude Code identity block. The subscription endpoint rejects requests
|
|
5909
|
-
* that do not present as Claude Code, so this block is REQUIRED as the first
|
|
5910
|
-
* system entry on every request.
|
|
5911
|
-
*/
|
|
5912
|
-
const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
|
|
5913
|
-
/** Tags wrapping a mid-conversation system message where it sits in the history. */
|
|
5914
|
-
const SYSTEM_REMINDER_OPEN = "<system-reminder>";
|
|
5915
|
-
const SYSTEM_REMINDER_CLOSE = "</system-reminder>";
|
|
5916
|
-
/**
|
|
5917
|
-
* How far apart consecutive message breakpoints sit, in content blocks.
|
|
5918
|
-
*
|
|
5919
|
-
* A breakpoint looks back at most 20 blocks for an entry an earlier request
|
|
5920
|
-
* wrote, so marks must stay closer than that: one agentic turn can append a
|
|
5921
|
-
* dozen tool_use/tool_result blocks at once, and a single trailing mark would
|
|
5922
|
-
* silently fall out of range and rebuild the whole prefix.
|
|
5923
|
-
*/
|
|
5924
|
-
const CACHE_BLOCK_STRIDE = 15;
|
|
5925
|
-
/**
|
|
5926
|
-
* Message breakpoints per request. Anthropic allows four in total and the
|
|
5927
|
-
* last `system` block takes the fourth, so three are left for the history —
|
|
5928
|
-
* enough to tolerate a turn appending roughly {@link CACHE_BLOCK_STRIDE} × 3
|
|
5929
|
-
* blocks before a read is lost.
|
|
5930
|
-
*/
|
|
5931
|
-
const MESSAGE_CACHE_BREAKPOINTS = 3;
|
|
5932
|
-
/** Preserve native image blocks, retaining the existing text-only wire shape. */
|
|
5933
|
-
function toolResultContent(block) {
|
|
5934
|
-
if (!block.content.some((part) => part.type === "image" && "dataBase64" in part)) return block.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
5935
|
-
const content = [];
|
|
5936
|
-
for (const part of block.content) {
|
|
5937
|
-
if (part.type === "text" && part.text.length > 0) content.push({
|
|
5938
|
-
type: "text",
|
|
5939
|
-
text: part.text
|
|
5940
|
-
});
|
|
5941
|
-
if (part.type === "image" && "dataBase64" in part) content.push({
|
|
5942
|
-
type: "image",
|
|
5943
|
-
source: {
|
|
5944
|
-
type: "base64",
|
|
5945
|
-
media_type: part.mediaType,
|
|
5946
|
-
data: part.dataBase64
|
|
5947
|
-
}
|
|
5948
|
-
});
|
|
5949
|
-
}
|
|
5950
|
-
return content;
|
|
5951
|
-
}
|
|
5952
|
-
/** Parse a tool call's raw JSON arguments into Anthropic's object-shaped `input`. */
|
|
5953
|
-
function parseToolInput(raw) {
|
|
5954
|
-
try {
|
|
5955
|
-
const parsed = JSON.parse(raw);
|
|
5956
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) return parsed;
|
|
5957
|
-
return {};
|
|
5958
|
-
} catch {
|
|
5959
|
-
return {};
|
|
5960
|
-
}
|
|
5961
|
-
}
|
|
5962
|
-
/**
|
|
5963
|
-
* Move a user message's `tool_result` blocks into one contiguous run at the
|
|
5964
|
-
* front, preserving the relative order of both groups.
|
|
5965
|
-
*
|
|
5966
|
-
* Anthropic answers every `tool_use` against the blocks that *lead* the next
|
|
5967
|
-
* message, so a block of any other kind before or between the results reads
|
|
5968
|
-
* as a call left unanswered and the request is rejected. The harness merges
|
|
5969
|
-
* everything queued for one user turn into a single message, and a parallel
|
|
5970
|
-
* tool batch arrives as one result message per call, so any context spliced
|
|
5971
|
-
* mid-batch lands between two results. Restoring the run here keeps that
|
|
5972
|
-
* independent of delivery order. Order *among* the results does not matter.
|
|
5973
|
-
* @param message - one assembled user message, reordered in place.
|
|
5729
|
+
* Move a user message's `tool_result` blocks into one contiguous run at the
|
|
5730
|
+
* front, preserving the relative order of both groups.
|
|
5731
|
+
*
|
|
5732
|
+
* Anthropic answers every `tool_use` against the blocks that *lead* the next
|
|
5733
|
+
* message, so a block of any other kind before or between the results reads
|
|
5734
|
+
* as a call left unanswered and the request is rejected. The harness merges
|
|
5735
|
+
* everything queued for one user turn into a single message, and a parallel
|
|
5736
|
+
* tool batch arrives as one result message per call, so any context spliced
|
|
5737
|
+
* mid-batch lands between two results. Restoring the run here keeps that
|
|
5738
|
+
* independent of delivery order. Order *among* the results does not matter.
|
|
5739
|
+
* @param message - one assembled user message, reordered in place.
|
|
5974
5740
|
*/
|
|
5975
5741
|
function leadWithToolResults(message) {
|
|
5976
5742
|
const firstOther = message.content.findIndex((block) => block.type !== "tool_result");
|
|
@@ -6095,13 +5861,14 @@ function toAnthropicMessages(messages) {
|
|
|
6095
5861
|
* Marks are counted across the flattened block sequence, not per message,
|
|
6096
5862
|
* because the lookback window Anthropic walks counts blocks the same way.
|
|
6097
5863
|
* @param messages - assembled Anthropic messages, marked in place.
|
|
5864
|
+
* @param ttl - cache lifetime for every mark; defaults to five minutes.
|
|
6098
5865
|
*/
|
|
6099
|
-
function markMessageCache(messages) {
|
|
5866
|
+
function markMessageCache(messages, ttl) {
|
|
6100
5867
|
const blocks = messages.flatMap((message) => message.content);
|
|
6101
5868
|
for (let mark = 0; mark < MESSAGE_CACHE_BREAKPOINTS; mark++) {
|
|
6102
5869
|
const at = blocks.length - 1 - mark * CACHE_BLOCK_STRIDE;
|
|
6103
5870
|
if (at < 0) return;
|
|
6104
|
-
blocks[at].cache_control =
|
|
5871
|
+
blocks[at].cache_control = cacheControl(ttl);
|
|
6105
5872
|
}
|
|
6106
5873
|
}
|
|
6107
5874
|
/**
|
|
@@ -6110,9 +5877,10 @@ function markMessageCache(messages) {
|
|
|
6110
5877
|
* @param system - explicit system prompt, when set.
|
|
6111
5878
|
* @param messages - conversation messages; the system-role text preceding the
|
|
6112
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.
|
|
6113
5881
|
* @returns the system content blocks.
|
|
6114
5882
|
*/
|
|
6115
|
-
function toAnthropicSystem(system, messages) {
|
|
5883
|
+
function toAnthropicSystem(system, messages, ttl) {
|
|
6116
5884
|
const blocks = [{
|
|
6117
5885
|
type: "text",
|
|
6118
5886
|
text: CLAUDE_CODE_IDENTITY
|
|
@@ -6126,7 +5894,7 @@ function toAnthropicSystem(system, messages) {
|
|
|
6126
5894
|
type: "text",
|
|
6127
5895
|
text: block.text
|
|
6128
5896
|
});
|
|
6129
|
-
blocks[blocks.length - 1].cache_control =
|
|
5897
|
+
blocks[blocks.length - 1].cache_control = cacheControl(ttl);
|
|
6130
5898
|
return blocks;
|
|
6131
5899
|
}
|
|
6132
5900
|
/**
|
|
@@ -6174,10 +5942,11 @@ function closeBlock$2(block) {
|
|
|
6174
5942
|
function anthropicFailure(error) {
|
|
6175
5943
|
const type = error?.type ?? "unknown_error";
|
|
6176
5944
|
const message = error?.message ?? `Anthropic reported ${type}`;
|
|
6177
|
-
|
|
6178
|
-
if (type === "
|
|
6179
|
-
if (type === "
|
|
6180
|
-
|
|
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);
|
|
6181
5950
|
}
|
|
6182
5951
|
/**
|
|
6183
5952
|
* Push-model Anthropic SSE translator: feed each parsed event object to
|
|
@@ -6366,27 +6135,426 @@ var AnthropicStreamTranslator = class {
|
|
|
6366
6135
|
case "error": throw anthropicFailure(event.error);
|
|
6367
6136
|
default: return chunks;
|
|
6368
6137
|
}
|
|
6369
|
-
}
|
|
6370
|
-
};
|
|
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
|
|
6371
6388
|
/**
|
|
6372
|
-
*
|
|
6373
|
-
*
|
|
6374
|
-
*
|
|
6375
|
-
*
|
|
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.
|
|
6376
6393
|
*/
|
|
6377
|
-
|
|
6378
|
-
const
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
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);
|
|
6385
6421
|
}
|
|
6386
|
-
yield* translator.push(event);
|
|
6387
|
-
if (translator.terminated) return;
|
|
6388
6422
|
}
|
|
6389
|
-
|
|
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
|
+
})));
|
|
6390
6558
|
}
|
|
6391
6559
|
|
|
6392
6560
|
//#endregion
|
|
@@ -6517,8 +6685,8 @@ const CLAUDE_RESET_FIELDS = [
|
|
|
6517
6685
|
* headers are deliberately not read: they are rollover snapshots attached to
|
|
6518
6686
|
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
6519
6687
|
* earliest of them is typically the bucket that still had room — a wait that
|
|
6520
|
-
* lands straight back in the closed window.
|
|
6521
|
-
*
|
|
6688
|
+
* lands straight back in the closed window. The parsed reset supplies the retry
|
|
6689
|
+
* hint; diagnostics omit raw provider headers and body text.
|
|
6522
6690
|
*/
|
|
6523
6691
|
const claudeRateLimitReset = (response, body, now) => {
|
|
6524
6692
|
const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
|
|
@@ -6713,11 +6881,49 @@ function isClaudePermanentRefreshError(error) {
|
|
|
6713
6881
|
return error instanceof OAuthEndpointError && (error.oauthCode === "invalid_grant" || error.oauthCode === "invalid_token");
|
|
6714
6882
|
}
|
|
6715
6883
|
const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
6716
|
-
/**
|
|
6884
|
+
/** ISO-8601 or numeric epoch (seconds below 1e10 in magnitude, otherwise ms). */
|
|
6717
6885
|
function claudeResetsAt(value) {
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
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;
|
|
6721
6927
|
}
|
|
6722
6928
|
/** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
|
|
6723
6929
|
function claudeWindowTiming(kind) {
|
|
@@ -6769,6 +6975,10 @@ function claudeLimitsWindows(value) {
|
|
|
6769
6975
|
* source of Claude Code's `/usage` screen). Newer responses carry a
|
|
6770
6976
|
* structured `limits` array; older ones the flat `five_hour`/`seven_day*`
|
|
6771
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.
|
|
6772
6982
|
* @param session - the stored session (used as-is; never refreshed here).
|
|
6773
6983
|
* @param fetchFn - fetch implementation (injectable for tests).
|
|
6774
6984
|
* @param signal - caller cancellation from the RPC transport.
|
|
@@ -6777,7 +6987,7 @@ function claudeLimitsWindows(value) {
|
|
|
6777
6987
|
*/
|
|
6778
6988
|
async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion = localClaudeCliVersion) {
|
|
6779
6989
|
const userAgent = claudeCliUserAgent(await cliVersion());
|
|
6780
|
-
const
|
|
6990
|
+
const init = {
|
|
6781
6991
|
headers: {
|
|
6782
6992
|
"authorization": `Bearer ${session.accessToken}`,
|
|
6783
6993
|
"anthropic-beta": "oauth-2025-04-20",
|
|
@@ -6785,13 +6995,25 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
|
|
|
6785
6995
|
"accept": "application/json"
|
|
6786
6996
|
},
|
|
6787
6997
|
...signal === void 0 ? {} : { signal }
|
|
6788
|
-
}
|
|
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
|
+
}
|
|
6789
7005
|
if (!response.ok) throw await oauthEndpointError(response, "claude usage");
|
|
6790
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
|
+
};
|
|
6791
7012
|
const modern = claudeLimitsWindows(payload.limits);
|
|
6792
7013
|
if (modern.length > 0) return {
|
|
6793
7014
|
supported: true,
|
|
6794
|
-
windows: modern
|
|
7015
|
+
windows: modern,
|
|
7016
|
+
...resets
|
|
6795
7017
|
};
|
|
6796
7018
|
const windows = [];
|
|
6797
7019
|
const legacy = [
|
|
@@ -6803,7 +7025,8 @@ async function fetchClaudeUsage(session, fetchFn = hostFetch, signal, cliVersion
|
|
|
6803
7025
|
for (const window of legacy) if (window !== void 0) windows.push(window);
|
|
6804
7026
|
return {
|
|
6805
7027
|
supported: true,
|
|
6806
|
-
windows
|
|
7028
|
+
windows,
|
|
7029
|
+
...resets
|
|
6807
7030
|
};
|
|
6808
7031
|
}
|
|
6809
7032
|
function claudeThinkingType(capabilities) {
|
|
@@ -6888,6 +7111,14 @@ const CLAUDE_IMAGE_LIMIT = {
|
|
|
6888
7111
|
maxEdge: 2e3,
|
|
6889
7112
|
maxBytes: 375e4
|
|
6890
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;
|
|
6891
7122
|
/** The Claude 4.5 family accepts image input. */
|
|
6892
7123
|
const CLAUDE_MODALITIES = ["text", "image"];
|
|
6893
7124
|
/**
|
|
@@ -6903,15 +7134,20 @@ const CLAUDE_MODALITIES = ["text", "image"];
|
|
|
6903
7134
|
* @param maxTokens - the resolved output cap.
|
|
6904
7135
|
* @param thinking - the thinking parameter, when the model takes one.
|
|
6905
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.
|
|
6906
7141
|
* @returns the JSON body to POST.
|
|
6907
7142
|
*/
|
|
6908
|
-
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;
|
|
6909
7145
|
const anthropicMessages = toAnthropicMessages(messages);
|
|
6910
|
-
markMessageCache(anthropicMessages);
|
|
7146
|
+
markMessageCache(anthropicMessages, ttl);
|
|
6911
7147
|
return {
|
|
6912
7148
|
model: options.model,
|
|
6913
7149
|
max_tokens: maxTokens,
|
|
6914
|
-
system: toAnthropicSystem(options.system, messages),
|
|
7150
|
+
system: toAnthropicSystem(options.system, messages, ttl),
|
|
6915
7151
|
messages: anthropicMessages,
|
|
6916
7152
|
...options.tools !== void 0 && options.tools.length > 0 ? { tools: toAnthropicTools(options.tools) } : {},
|
|
6917
7153
|
...thinking === void 0 ? {} : { thinking },
|
|
@@ -7097,9 +7333,13 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
7097
7333
|
}
|
|
7098
7334
|
async request(options, session, signal) {
|
|
7099
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
|
+
}
|
|
7100
7340
|
const disc = await this.discovered(options.model);
|
|
7101
7341
|
const maxTokens = options.maxTokens ?? claudeMaxTokens(this.options.models.find((entry) => entry.id === options.model), disc);
|
|
7102
|
-
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);
|
|
7103
7343
|
const cliVersion = await (this.options.resolveCliVersion ?? localClaudeCliVersion)();
|
|
7104
7344
|
return hostFetch(CLAUDE_API_URL, {
|
|
7105
7345
|
method: "POST",
|
|
@@ -7293,11 +7533,11 @@ function mapResponsesUsage(usage) {
|
|
|
7293
7533
|
* @returns the mapped error (context overflow, quota, otherwise SERVER).
|
|
7294
7534
|
*/
|
|
7295
7535
|
function responsesFailure(code, message) {
|
|
7296
|
-
const text = message ?? code ?? "the provider reported a failed response";
|
|
7297
7536
|
const detail = `${code ?? ""} ${message ?? ""}`;
|
|
7298
|
-
|
|
7299
|
-
if (code
|
|
7300
|
-
|
|
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);
|
|
7301
7541
|
}
|
|
7302
7542
|
/** Assemble the final ContentBlock for one open block. */
|
|
7303
7543
|
function closeBlock$1(block) {
|
|
@@ -7516,7 +7756,7 @@ async function* streamResponses(stream, onActivity, transform) {
|
|
|
7516
7756
|
try {
|
|
7517
7757
|
event = JSON.parse(sseEvent.data);
|
|
7518
7758
|
} catch {
|
|
7519
|
-
throw new LlmError(
|
|
7759
|
+
throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
7520
7760
|
}
|
|
7521
7761
|
if (transform !== void 0) event = transform(event);
|
|
7522
7762
|
yield* translator.push(event);
|
|
@@ -7555,8 +7795,8 @@ const CODEX_RESET_FIELDS = [
|
|
|
7555
7795
|
* rollover snapshots the backend attaches to every response, one per window,
|
|
7556
7796
|
* so they say nothing about which window refused: a burst 429 that would clear
|
|
7557
7797
|
* in seconds still carries a primary rollover hours out, and reading it would
|
|
7558
|
-
* park the turn for those hours.
|
|
7559
|
-
*
|
|
7798
|
+
* park the turn for those hours. The parsed body reset supplies the retry hint;
|
|
7799
|
+
* diagnostics omit raw provider headers and body text.
|
|
7560
7800
|
*/
|
|
7561
7801
|
const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
7562
7802
|
/** Default instruction when the request carries no system prompt. */
|
|
@@ -7728,6 +7968,63 @@ function isCodexPermanentRefreshError(error) {
|
|
|
7728
7968
|
return error instanceof OAuthEndpointError && error.oauthCode !== void 0 && PERMANENT_REFRESH_CODES.has(error.oauthCode);
|
|
7729
7969
|
}
|
|
7730
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
|
+
}
|
|
7731
8028
|
/** Seconds of the canonical 5-hour session and 7-day weekly windows. */
|
|
7732
8029
|
const SESSION_WINDOW_SECONDS = 300 * 60;
|
|
7733
8030
|
const WEEKLY_WINDOW_SECONDS = 10080 * 60;
|
|
@@ -7851,7 +8148,7 @@ async function fetchCodexModels(session, fetchFn = hostFetch, signal, clientVers
|
|
|
7851
8148
|
for (const entry of payload.models) {
|
|
7852
8149
|
if (typeof entry.slug !== "string" || entry.slug.length === 0) continue;
|
|
7853
8150
|
if (entry.visibility === "hide" || entry.visibility === "none") continue;
|
|
7854
|
-
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) => ({
|
|
7855
8152
|
id: ReasoningEffortId(level.effort),
|
|
7856
8153
|
name: effortDisplayName(level.effort),
|
|
7857
8154
|
...level.description === void 0 ? {} : { description: level.description }
|
|
@@ -7952,6 +8249,16 @@ function reconcileResponsesToolCalls(input) {
|
|
|
7952
8249
|
return balanced;
|
|
7953
8250
|
}
|
|
7954
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
|
+
/**
|
|
7955
8262
|
* The Responses request body for one generation. A fast-tier request (the
|
|
7956
8263
|
* composer Speed toggle, the codex CLI's fast mode) carries
|
|
7957
8264
|
* `service_tier: priority`; the tier field is omitted entirely otherwise,
|
|
@@ -7968,7 +8275,7 @@ function codexRequestBody(options, resolved, fast) {
|
|
|
7968
8275
|
parallel_tool_calls: true
|
|
7969
8276
|
} : {},
|
|
7970
8277
|
...options.reasoningEffort !== void 0 ? { reasoning: {
|
|
7971
|
-
effort: String(options.reasoningEffort),
|
|
8278
|
+
effort: codexWireEffort(String(options.reasoningEffort)),
|
|
7972
8279
|
summary: "auto"
|
|
7973
8280
|
} } : {},
|
|
7974
8281
|
store: false,
|
|
@@ -8401,10 +8708,10 @@ function abortableDelay(ms, signal) {
|
|
|
8401
8708
|
throwIfAborted(signal);
|
|
8402
8709
|
return Promise.resolve();
|
|
8403
8710
|
}
|
|
8404
|
-
return new Promise((resolve$
|
|
8711
|
+
return new Promise((resolve$2, reject) => {
|
|
8405
8712
|
const timer = setTimeout(() => {
|
|
8406
8713
|
cleanup();
|
|
8407
|
-
resolve$
|
|
8714
|
+
resolve$2();
|
|
8408
8715
|
}, ms);
|
|
8409
8716
|
const onAbort = () => {
|
|
8410
8717
|
cleanup();
|
|
@@ -8625,6 +8932,12 @@ var PoolHealthRegistry = class {
|
|
|
8625
8932
|
}
|
|
8626
8933
|
return earliest;
|
|
8627
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
|
+
}
|
|
8628
8941
|
/** Drop records of one provider, or of a single account when given (auth changes). */
|
|
8629
8942
|
clear(provider, account) {
|
|
8630
8943
|
const prefix = account === void 0 ? `${provider}/` : `${provider}/${account}/`;
|
|
@@ -8632,6 +8945,67 @@ var PoolHealthRegistry = class {
|
|
|
8632
8945
|
}
|
|
8633
8946
|
};
|
|
8634
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
|
+
|
|
8635
9009
|
//#endregion
|
|
8636
9010
|
//#region src/providers/pool.ts
|
|
8637
9011
|
/** Bound on sticky-session memory; oldest entries evict past it. */
|
|
@@ -8810,34 +9184,39 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8810
9184
|
...modalities === void 0 ? {} : { inputModalities: modalities }
|
|
8811
9185
|
};
|
|
8812
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
|
+
*/
|
|
8813
9193
|
async *stream(options) {
|
|
8814
9194
|
const definition = (await this.pools()).get(poolKey(options.provider, options.model));
|
|
8815
9195
|
if (definition === void 0) throw new LlmError(`unknown pool model "${options.model}"`, "NO_ADAPTER");
|
|
8816
9196
|
const members = await this.concrete(definition.members);
|
|
8817
9197
|
const candidates = await this.select(options.model, members, options.sessionId);
|
|
8818
9198
|
if (candidates.length === 0) throw this.exhausted(options.model, members);
|
|
8819
|
-
|
|
9199
|
+
const failures = [];
|
|
8820
9200
|
for (const member of candidates) {
|
|
8821
9201
|
const adapter = this.options.adapters[member.provider];
|
|
8822
9202
|
if (adapter === void 0) continue;
|
|
8823
|
-
const iterator = adapter
|
|
9203
|
+
const iterator = streamAccountWithReplay(adapter, {
|
|
8824
9204
|
...options,
|
|
8825
|
-
provider: member.provider
|
|
8826
|
-
|
|
8827
|
-
}, member.account)[Symbol.asyncIterator]();
|
|
9205
|
+
provider: member.provider
|
|
9206
|
+
}, member.account, member.model)[Symbol.asyncIterator]();
|
|
8828
9207
|
let first;
|
|
8829
9208
|
try {
|
|
8830
9209
|
first = await iterator.next();
|
|
8831
9210
|
if (first.done === true) throw new LlmError(`${memberLabel(member)} returned an empty stream`, EMPTY_RESPONSE_CODE);
|
|
8832
9211
|
} catch (error) {
|
|
8833
9212
|
const classification = classifyPoolFailure(error, member.provider);
|
|
8834
|
-
if (classification.action === "throw") throw error;
|
|
9213
|
+
if (classification.action === "throw" || !(error instanceof LlmError)) throw error;
|
|
8835
9214
|
if ("cooldownMs" in classification) {
|
|
8836
9215
|
this.options.health.markUnavailable(classification.scope === "account" ? accountKey(member.provider, member.account) : memberKey(member.provider, member.account, member.model), classification.cooldownMs, classification.reason);
|
|
8837
9216
|
if (classification.reason === QUOTA_EXCEEDED_CODE || classification.reason === "RATE_LIMIT") this.options.usage.invalidate(member.provider, member.account);
|
|
8838
9217
|
}
|
|
8839
9218
|
this.options.onWarn(`pool "${options.model}": ${memberLabel(member)} failed before any output (${error instanceof Error ? error.message : String(error)}); trying the next member`);
|
|
8840
|
-
|
|
9219
|
+
failures.push(error);
|
|
8841
9220
|
continue;
|
|
8842
9221
|
}
|
|
8843
9222
|
this.remember(options.model, options.sessionId, member);
|
|
@@ -8851,7 +9230,7 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8851
9230
|
}
|
|
8852
9231
|
return;
|
|
8853
9232
|
}
|
|
8854
|
-
throw this.exhausted(options.model, members,
|
|
9233
|
+
throw this.exhausted(options.model, members, failures);
|
|
8855
9234
|
}
|
|
8856
9235
|
/**
|
|
8857
9236
|
* Order the candidates for one request. Health filters both strategies;
|
|
@@ -8892,12 +9271,9 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8892
9271
|
}
|
|
8893
9272
|
this.sticky.set(key, memberKey(member.provider, member.account, member.model));
|
|
8894
9273
|
}
|
|
8895
|
-
/**
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
* hint is scoped to the keys this pool can actually recover through).
|
|
8899
|
-
*/
|
|
8900
|
-
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");
|
|
8901
9277
|
const keys = /* @__PURE__ */ new Set();
|
|
8902
9278
|
for (const member of pool) {
|
|
8903
9279
|
keys.add(memberKey(member.provider, member.account, member.model));
|
|
@@ -8905,12 +9281,53 @@ var PoolAdapter = class extends LlmAdapter {
|
|
|
8905
9281
|
}
|
|
8906
9282
|
const recovery = this.options.health.earliestRecovery(keys);
|
|
8907
9283
|
const retryAfterMs$1 = recovery === void 0 ? void 0 : Math.max(recovery - Date.now(), 1);
|
|
8908
|
-
|
|
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), {
|
|
8909
9300
|
...retryAfterMs$1 === void 0 ? {} : { providerRetryAfterMs: retryAfterMs$1 },
|
|
8910
9301
|
...cause === void 0 ? {} : { cause }
|
|
8911
9302
|
});
|
|
8912
9303
|
}
|
|
8913
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
|
+
}
|
|
8914
9331
|
function stickyKey(poolId, sessionId) {
|
|
8915
9332
|
return `${String(sessionId)}|${poolId}`;
|
|
8916
9333
|
}
|
|
@@ -9067,10 +9484,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
|
|
|
9067
9484
|
const independent = parseAccountModelId(options.model);
|
|
9068
9485
|
if (independent) {
|
|
9069
9486
|
await this.requireAccount(independent.account, independent.model, true);
|
|
9070
|
-
yield* this.options.adapter.
|
|
9071
|
-
...options,
|
|
9072
|
-
model: independent.model
|
|
9073
|
-
}, independent.account);
|
|
9487
|
+
yield* streamAccountWithReplay(this.options.adapter, options, independent.account, independent.model);
|
|
9074
9488
|
return;
|
|
9075
9489
|
}
|
|
9076
9490
|
const pool = this.options.pool();
|
|
@@ -9078,7 +9492,7 @@ var AccountPreferencesAdapter = class extends LlmAdapter {
|
|
|
9078
9492
|
yield* pool.stream(options);
|
|
9079
9493
|
return;
|
|
9080
9494
|
}
|
|
9081
|
-
yield* this.options.adapter
|
|
9495
|
+
yield* streamAccountWithReplay(this.options.adapter, options, await this.fallback(options.model));
|
|
9082
9496
|
}
|
|
9083
9497
|
};
|
|
9084
9498
|
|
|
@@ -9227,6 +9641,11 @@ const FALLBACK_HORIZON_MS = {
|
|
|
9227
9641
|
var PoolUsageTracker = class {
|
|
9228
9642
|
entries = /* @__PURE__ */ new Map();
|
|
9229
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();
|
|
9230
9649
|
constructor(fetcherFor, ttlMs = USAGE_TTL_MS) {
|
|
9231
9650
|
this.fetcherFor = fetcherFor;
|
|
9232
9651
|
this.ttlMs = ttlMs;
|
|
@@ -9309,13 +9728,25 @@ var PoolUsageTracker = class {
|
|
|
9309
9728
|
throw error;
|
|
9310
9729
|
}
|
|
9311
9730
|
}
|
|
9312
|
-
/**
|
|
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
|
+
*/
|
|
9313
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
|
+
};
|
|
9314
9745
|
if (account !== void 0) {
|
|
9315
|
-
|
|
9746
|
+
drop(`${provider}/${account}`);
|
|
9316
9747
|
return;
|
|
9317
9748
|
}
|
|
9318
|
-
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);
|
|
9319
9750
|
}
|
|
9320
9751
|
/**
|
|
9321
9752
|
* Run (or join) the single in-flight fetch for one account key, caching
|
|
@@ -9330,6 +9761,8 @@ var PoolUsageTracker = class {
|
|
|
9330
9761
|
if (pending === void 0) {
|
|
9331
9762
|
const prior = this.entries.get(key);
|
|
9332
9763
|
const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
|
|
9764
|
+
const generation = this.generation.get(key) ?? 0;
|
|
9765
|
+
const sequence = ++this.nextSequence;
|
|
9333
9766
|
pending = fetcher().then((value) => {
|
|
9334
9767
|
const at = Date.now();
|
|
9335
9768
|
const snapshot = {
|
|
@@ -9337,21 +9770,27 @@ var PoolUsageTracker = class {
|
|
|
9337
9770
|
observedAt: at,
|
|
9338
9771
|
stale: false
|
|
9339
9772
|
};
|
|
9340
|
-
this.
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
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
|
+
}
|
|
9344
9780
|
return snapshot;
|
|
9345
9781
|
}, (error) => {
|
|
9346
|
-
if (!isMissingOrInvalidCredential(error)) this.
|
|
9347
|
-
|
|
9348
|
-
|
|
9349
|
-
|
|
9350
|
-
|
|
9351
|
-
|
|
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
|
+
}
|
|
9352
9791
|
throw error;
|
|
9353
9792
|
}).finally(() => {
|
|
9354
|
-
this.inflight.delete(key);
|
|
9793
|
+
if (this.inflight.get(key) === pending) this.inflight.delete(key);
|
|
9355
9794
|
});
|
|
9356
9795
|
this.inflight.set(key, pending);
|
|
9357
9796
|
}
|
|
@@ -9433,8 +9872,8 @@ const GROK_RESET_FIELDS = [
|
|
|
9433
9872
|
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
9434
9873
|
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
9435
9874
|
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
9436
|
-
* which would burn the whole retry budget in seconds.
|
|
9437
|
-
*
|
|
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.
|
|
9438
9877
|
*/
|
|
9439
9878
|
const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
|
|
9440
9879
|
/** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
|
|
@@ -10382,7 +10821,7 @@ async function* streamChatCompletions(stream, onActivity) {
|
|
|
10382
10821
|
try {
|
|
10383
10822
|
event = JSON.parse(sseEvent.data);
|
|
10384
10823
|
} catch {
|
|
10385
|
-
throw new LlmError(
|
|
10824
|
+
throw new LlmError("malformed SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
10386
10825
|
}
|
|
10387
10826
|
yield* translator.push(event);
|
|
10388
10827
|
if (translator.terminated) return;
|
|
@@ -10969,13 +11408,16 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
10969
11408
|
* account identity is the session's long-lived GitHub token (stable across
|
|
10970
11409
|
* Copilot-token refreshes, different per GitHub login); the conversation is
|
|
10971
11410
|
* the loop-stamped `sessionId`, falling back to the first message's id
|
|
10972
|
-
* 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
|
|
10973
11413
|
* wire families. A call id captured in one scope is invisible to every
|
|
10974
11414
|
* other scope, so reused ids cannot leak reasoning across accounts,
|
|
10975
11415
|
* conversations, or models.
|
|
10976
11416
|
*/
|
|
10977
11417
|
replayScope(tokenKey, options) {
|
|
10978
|
-
|
|
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}`;
|
|
10979
11421
|
}
|
|
10980
11422
|
/**
|
|
10981
11423
|
* Store one response's completed reasoning items behind every call id it
|
|
@@ -10988,6 +11430,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
10988
11430
|
* per call.
|
|
10989
11431
|
*/
|
|
10990
11432
|
captureReasoning(scope, callIds, items) {
|
|
11433
|
+
if (scope === void 0) return;
|
|
10991
11434
|
let entries = this.replayByScope.get(scope);
|
|
10992
11435
|
if (entries === void 0) {
|
|
10993
11436
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -11023,6 +11466,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
11023
11466
|
* degradation, never an error.
|
|
11024
11467
|
*/
|
|
11025
11468
|
replayFor(scope, callId) {
|
|
11469
|
+
if (scope === void 0) return void 0;
|
|
11026
11470
|
const entries = this.replayByScope.get(scope);
|
|
11027
11471
|
const entry = entries?.get(callId);
|
|
11028
11472
|
if (entries === void 0 || entry === void 0) return void 0;
|
|
@@ -11178,8 +11622,8 @@ function object(value) {
|
|
|
11178
11622
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11179
11623
|
}
|
|
11180
11624
|
/** Resolve local references before removing definitions; reject cycles and remote references. */
|
|
11181
|
-
function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
11182
|
-
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));
|
|
11183
11627
|
if (!object(value)) return value;
|
|
11184
11628
|
if (typeof value.$ref === "string") {
|
|
11185
11629
|
const ref = value.$ref;
|
|
@@ -11191,7 +11635,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
|
11191
11635
|
}
|
|
11192
11636
|
if (!object(target)) throw new LlmError(`Antigravity tool schema reference is missing: ${ref}`, "INVALID_REQUEST");
|
|
11193
11637
|
const { $ref: _ref,...siblings } = value;
|
|
11194
|
-
return resolve({
|
|
11638
|
+
return resolve$1({
|
|
11195
11639
|
...target,
|
|
11196
11640
|
...siblings
|
|
11197
11641
|
}, root, new Set([...refs, ref]));
|
|
@@ -11201,7 +11645,7 @@ function resolve(value, root, refs = /* @__PURE__ */ new Set()) {
|
|
|
11201
11645
|
"$id",
|
|
11202
11646
|
"$defs",
|
|
11203
11647
|
"definitions"
|
|
11204
|
-
].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)]));
|
|
11205
11649
|
}
|
|
11206
11650
|
/** Numeric types widen to `number`: every integer is also a number. */
|
|
11207
11651
|
const NUMERIC = new Set(["integer", "number"]);
|
|
@@ -11287,7 +11731,7 @@ function custom(value) {
|
|
|
11287
11731
|
}
|
|
11288
11732
|
/** Detach and normalize one tool's root object without changing the registry schema. */
|
|
11289
11733
|
function antigravityToolParameters(parameters, legacy) {
|
|
11290
|
-
const resolved = resolve(parameters, parameters);
|
|
11734
|
+
const resolved = resolve$1(parameters, parameters);
|
|
11291
11735
|
const root = {
|
|
11292
11736
|
...resolved,
|
|
11293
11737
|
type: resolved.type ?? "object"
|
|
@@ -11303,9 +11747,11 @@ function antigravityToolParameters(parameters, legacy) {
|
|
|
11303
11747
|
* `FunctionResponse.response` field requires. That field maps to a singular
|
|
11304
11748
|
* protobuf `Struct`, so a bare array, string, number or boolean is rejected
|
|
11305
11749
|
* by the endpoint with "Proto field is not repeating, cannot start list".
|
|
11306
|
-
*
|
|
11750
|
+
* Non-array JSON objects use their own fields; every other JSON value is
|
|
11307
11751
|
* wrapped in the same "output" envelope the non-JSON path already uses --
|
|
11308
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.
|
|
11309
11755
|
*/
|
|
11310
11756
|
function toolResultValue(block) {
|
|
11311
11757
|
const text = block.content.map((part) => part.type === "text" ? part.text : "").join("");
|
|
@@ -11318,8 +11764,17 @@ function toolResultValue(block) {
|
|
|
11318
11764
|
...block.isError === true ? { isError: true } : {}
|
|
11319
11765
|
};
|
|
11320
11766
|
}
|
|
11321
|
-
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed))
|
|
11322
|
-
|
|
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
|
+
};
|
|
11323
11778
|
}
|
|
11324
11779
|
/** Safely read per-block replay metadata emitted by this adapter. */
|
|
11325
11780
|
function replayBlocks(message, model) {
|
|
@@ -11369,7 +11824,8 @@ function toAntigravityContents(messages, model) {
|
|
|
11369
11824
|
response: toolResultValue({
|
|
11370
11825
|
type: "tool-result",
|
|
11371
11826
|
toolCallId: ToolCallId$1(id),
|
|
11372
|
-
content: message.content
|
|
11827
|
+
content: message.content,
|
|
11828
|
+
...message.isError === void 0 ? {} : { isError: message.isError }
|
|
11373
11829
|
})
|
|
11374
11830
|
} };
|
|
11375
11831
|
const previous$1 = out.at(-1);
|
|
@@ -11657,7 +12113,7 @@ async function* streamAntigravity(stream, onActivity) {
|
|
|
11657
12113
|
try {
|
|
11658
12114
|
parsed = JSON.parse(event.data);
|
|
11659
12115
|
} catch {
|
|
11660
|
-
throw new LlmError(
|
|
12116
|
+
throw new LlmError("malformed Antigravity SSE payload (MALFORMED_RESPONSE): [provider response body omitted]", "MALFORMED_RESPONSE");
|
|
11661
12117
|
}
|
|
11662
12118
|
yield* translator.push(parsed);
|
|
11663
12119
|
if (translator.terminated) return;
|
|
@@ -11821,7 +12277,7 @@ async function discoverAntigravityAccount(accessToken, runtime = {}, fetchFn = h
|
|
|
11821
12277
|
projectId = projectIdOf(result.response?.cloudaicompanionProject);
|
|
11822
12278
|
break;
|
|
11823
12279
|
}
|
|
11824
|
-
await new Promise((resolve$
|
|
12280
|
+
await new Promise((resolve$2) => setTimeout(resolve$2, 1e3));
|
|
11825
12281
|
}
|
|
11826
12282
|
}
|
|
11827
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");
|
|
@@ -12847,14 +13303,14 @@ function truncate(text, max = 60) {
|
|
|
12847
13303
|
/** Abort-aware sleep between two polls. */
|
|
12848
13304
|
function sleep(ms, signal) {
|
|
12849
13305
|
if (ms <= 0) return Promise.resolve();
|
|
12850
|
-
return new Promise((resolve$
|
|
13306
|
+
return new Promise((resolve$2, reject) => {
|
|
12851
13307
|
const onAbort = () => {
|
|
12852
13308
|
clearTimeout(timer);
|
|
12853
13309
|
reject(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("video_generate: aborted"));
|
|
12854
13310
|
};
|
|
12855
13311
|
const timer = setTimeout(() => {
|
|
12856
13312
|
signal.removeEventListener("abort", onAbort);
|
|
12857
|
-
resolve$
|
|
13313
|
+
resolve$2();
|
|
12858
13314
|
}, ms);
|
|
12859
13315
|
if (signal.aborted) {
|
|
12860
13316
|
onAbort();
|
|
@@ -13168,6 +13624,7 @@ const Config = z.object({
|
|
|
13168
13624
|
]),
|
|
13169
13625
|
codexClientVersion: z.string(),
|
|
13170
13626
|
streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
13627
|
+
claudePromptCacheTtl: z.union(["5m", "1h"]).default(DEFAULT_PROMPT_CACHE_TTL),
|
|
13171
13628
|
rateLimit: z.object({
|
|
13172
13629
|
wait: z.boolean().default(true),
|
|
13173
13630
|
maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
|
|
@@ -13320,16 +13777,16 @@ const DEFAULT_MODELS = {
|
|
|
13320
13777
|
};
|
|
13321
13778
|
/** Validate and detach the model catalog for every provider. */
|
|
13322
13779
|
function resolveCatalog(models) {
|
|
13323
|
-
const resolve$
|
|
13780
|
+
const resolve$2 = (provider) => {
|
|
13324
13781
|
const configured = models?.[provider];
|
|
13325
13782
|
return validateModels(configured !== void 0 && configured.length > 0 ? configured : DEFAULT_MODELS[provider], `${name}: models.${provider}`);
|
|
13326
13783
|
};
|
|
13327
13784
|
return {
|
|
13328
|
-
codex: resolve$
|
|
13329
|
-
claude: resolve$
|
|
13330
|
-
grok: resolve$
|
|
13331
|
-
copilot: resolve$
|
|
13332
|
-
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")
|
|
13333
13790
|
};
|
|
13334
13791
|
}
|
|
13335
13792
|
/** The display account of a stored session, for the status endpoint. */
|
|
@@ -13399,7 +13856,7 @@ var SubscriptionsAuthController = class {
|
|
|
13399
13856
|
* cannot be read off the flow manager.
|
|
13400
13857
|
*/
|
|
13401
13858
|
claims = /* @__PURE__ */ new Map();
|
|
13402
|
-
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) {
|
|
13403
13860
|
this.flows = flows;
|
|
13404
13861
|
this.deviceFlows = deviceFlows;
|
|
13405
13862
|
this.onAuthChanged = onAuthChanged;
|
|
@@ -13409,6 +13866,15 @@ var SubscriptionsAuthController = class {
|
|
|
13409
13866
|
this.poolUsage = poolUsage;
|
|
13410
13867
|
this.antigravityConfig = antigravityConfig;
|
|
13411
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)]));
|
|
13412
13878
|
}
|
|
13413
13879
|
usage(provider, account, signal, force = false) {
|
|
13414
13880
|
const fetcher = this.usageFetchers[provider];
|
|
@@ -13599,8 +14065,8 @@ function presentedVersion(cache, waitMs = STATUS_VERSION_WAIT_MS) {
|
|
|
13599
14065
|
return cache.current();
|
|
13600
14066
|
}
|
|
13601
14067
|
let timer;
|
|
13602
|
-
await Promise.race([cache.resolve(), new Promise((resolve$
|
|
13603
|
-
timer = setTimeout(resolve$
|
|
14068
|
+
await Promise.race([cache.resolve(), new Promise((resolve$2) => {
|
|
14069
|
+
timer = setTimeout(resolve$2, waitMs);
|
|
13604
14070
|
})]).finally(() => {
|
|
13605
14071
|
clearTimeout(timer);
|
|
13606
14072
|
});
|
|
@@ -13619,6 +14085,8 @@ function apply(ctx, config) {
|
|
|
13619
14085
|
const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
|
|
13620
14086
|
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
13621
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'`);
|
|
13622
14090
|
const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
|
|
13623
14091
|
const catalog = resolveCatalog(config.models);
|
|
13624
14092
|
const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
|
|
@@ -13686,7 +14154,22 @@ function apply(ctx, config) {
|
|
|
13686
14154
|
});
|
|
13687
14155
|
codexTokens = tokens;
|
|
13688
14156
|
accountTokens.set("codex", tokens);
|
|
13689
|
-
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
|
+
};
|
|
13690
14173
|
let adapter;
|
|
13691
14174
|
adapter = new CodexAdapter({
|
|
13692
14175
|
...config.codexClientVersion === void 0 ? {} : { clientVersion: config.codexClientVersion },
|
|
@@ -13737,6 +14220,7 @@ function apply(ctx, config) {
|
|
|
13737
14220
|
catalogStore: catalogStore("claude"),
|
|
13738
14221
|
defaultEffortOf: (model) => defaultEffortOf("claude", model),
|
|
13739
14222
|
resolveCliVersion: () => claudeVersion.resolve(),
|
|
14223
|
+
promptCacheTtl: claudePromptCacheTtl,
|
|
13740
14224
|
pool: () => poolAdapter
|
|
13741
14225
|
});
|
|
13742
14226
|
adapters.set("claude", adapter);
|
|
@@ -13844,8 +14328,8 @@ function apply(ctx, config) {
|
|
|
13844
14328
|
const fetcherFor = (provider, account) => {
|
|
13845
14329
|
switch (provider) {
|
|
13846
14330
|
case "codex": {
|
|
13847
|
-
const
|
|
13848
|
-
return
|
|
14331
|
+
const fetcher = usageFetchers.codex;
|
|
14332
|
+
return fetcher === void 0 ? void 0 : () => fetcher(account, AbortSignal.timeout(POOL_USAGE_TIMEOUT_MS));
|
|
13849
14333
|
}
|
|
13850
14334
|
case "claude": {
|
|
13851
14335
|
const tokens = claudeTokens;
|
|
@@ -14020,13 +14504,20 @@ function apply(ctx, config) {
|
|
|
14020
14504
|
cursorHandle = credentialsCtx.llm.registerAdapter(["cursor-subscription"], cursorAdapter);
|
|
14021
14505
|
});
|
|
14022
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));
|
|
14023
14514
|
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage, config.antigravity, {
|
|
14024
14515
|
...providers.includes("codex") ? { codex: config.codexClientVersion === void 0 ? presentedVersion(codexVersion) : async () => ({
|
|
14025
14516
|
version: config.codexClientVersion,
|
|
14026
14517
|
source: "config"
|
|
14027
14518
|
}) } : {},
|
|
14028
14519
|
...providers.includes("claude") ? { claude: presentedVersion(claudeVersion) } : {}
|
|
14029
|
-
}), speed, modelDefaults, {
|
|
14520
|
+
}, resetRedemption), speed, modelDefaults, {
|
|
14030
14521
|
async get(provider, force) {
|
|
14031
14522
|
await loadModelDefaults();
|
|
14032
14523
|
const adapter = adapters.get(provider);
|