dsh-plugin-subscriptions 0.5.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -6
- package/README.zh.md +43 -4
- package/lib/auth/rpc.d.ts +36 -2
- package/lib/auth/rpc.js +47 -5
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/SpeedSelect.d.ts +25 -2
- package/lib/client/SpeedSelect.js +10 -6
- package/lib/client/SubscriptionsSection.d.ts +74 -0
- package/lib/client/SubscriptionsSection.js +325 -4
- package/lib/client/VideoGenerateToolview.d.ts +1 -1
- package/lib/client/index.d.ts +1 -9
- package/lib/client/index.js +7 -4
- package/lib/client/locales.d.ts +28 -0
- package/lib/client/locales.js +28 -0
- package/lib/client.js +458 -10
- package/lib/client.js.map +1 -1
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +865 -111
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/providers/claude.d.ts +24 -3
- package/lib/providers/claude.js +35 -24
- package/lib/providers/codex.d.ts +21 -0
- package/lib/providers/codex.js +37 -10
- package/lib/providers/common.d.ts +70 -6
- package/lib/providers/common.js +118 -19
- package/lib/providers/copilot.d.ts +10 -0
- package/lib/providers/copilot.js +21 -8
- package/lib/providers/grok.d.ts +21 -0
- package/lib/providers/grok.js +37 -7
- package/lib/providers/pool-usage.d.ts +23 -2
- package/lib/providers/pool-usage.js +70 -15
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/translate/anthropic.js +5 -4
- package/lib/translate/chat-completions.js +5 -4
- package/lib/translate/responses.js +5 -4
- package/package.json +21 -21
- package/lib/providers/antigravity.d.ts +0 -90
- package/lib/providers/antigravity.js +0 -392
- package/lib/translate/antigravity.d.ts +0 -110
- package/lib/translate/antigravity.js +0 -303
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
-
import
|
|
2
|
+
import * as llm from "@deepseek-ai/dsh-llm";
|
|
3
|
+
import { CONTEXT_WINDOW_EXCEEDED_CODE, EMPTY_RESPONSE_CODE, LlmAdapter, LlmError, QUOTA_EXCEEDED_CODE, ReasoningEffortId, attributionHeaders, errorChain, isContextWindowExceededError, isQuotaExceededError, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
3
4
|
import { createServer } from "node:http";
|
|
4
5
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
5
6
|
import { ProxyAgent, fetch as fetch$1 } from "undici";
|
|
@@ -271,13 +272,13 @@ const DISABLED = {
|
|
|
271
272
|
bypass: []
|
|
272
273
|
};
|
|
273
274
|
/** Current config; updated by every load/apply/save. */
|
|
274
|
-
let current = DISABLED;
|
|
275
|
+
let current$1 = DISABLED;
|
|
275
276
|
/** The live dispatcher, or undefined when proxies are off/errored. */
|
|
276
277
|
let agent;
|
|
277
278
|
/** Last load/apply failure, surfaced by the config view. */
|
|
278
279
|
let configError;
|
|
279
280
|
/** One lazy load of the on-disk config (module-import cheap; file read once). */
|
|
280
|
-
let ready;
|
|
281
|
+
let ready$1;
|
|
281
282
|
/** Absolute path of the proxy config file. */
|
|
282
283
|
function proxyFilePath() {
|
|
283
284
|
return dshHomePath("plugins", "subscriptions", "proxy.json");
|
|
@@ -392,7 +393,7 @@ async function applyConfig(cfg) {
|
|
|
392
393
|
withError(error);
|
|
393
394
|
next = void 0;
|
|
394
395
|
}
|
|
395
|
-
current = cfg;
|
|
396
|
+
current$1 = cfg;
|
|
396
397
|
}
|
|
397
398
|
const previous = agent;
|
|
398
399
|
agent = next;
|
|
@@ -432,16 +433,16 @@ async function loadConfigFile(path) {
|
|
|
432
433
|
});
|
|
433
434
|
}
|
|
434
435
|
/** Resolve the module state once from disk; failures disable the proxy. */
|
|
435
|
-
async function ensureReady() {
|
|
436
|
-
ready ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
|
|
436
|
+
async function ensureReady$1() {
|
|
437
|
+
ready$1 ??= loadConfigFile(proxyFilePath()).then(async (cfg) => {
|
|
437
438
|
await applyConfig(cfg);
|
|
438
|
-
return current;
|
|
439
|
+
return current$1;
|
|
439
440
|
}, async (error) => {
|
|
440
441
|
withError(error);
|
|
441
442
|
await applyConfig(void 0);
|
|
442
|
-
return current;
|
|
443
|
+
return current$1;
|
|
443
444
|
});
|
|
444
|
-
return ready;
|
|
445
|
+
return ready$1;
|
|
445
446
|
}
|
|
446
447
|
/** Persist a config atomically with owner-only permissions, then apply it. */
|
|
447
448
|
async function persistConfig(cfg, path) {
|
|
@@ -462,13 +463,13 @@ async function persistConfig(cfg, path) {
|
|
|
462
463
|
* load/apply failure when the stored config is unusable.
|
|
463
464
|
*/
|
|
464
465
|
async function proxyGetConfig() {
|
|
465
|
-
await ensureReady();
|
|
466
|
+
await ensureReady$1();
|
|
466
467
|
return {
|
|
467
|
-
enabled: current.enabled,
|
|
468
|
-
url: current.url,
|
|
469
|
-
...current.username === void 0 ? {} : { username: current.username },
|
|
470
|
-
passwordSet: current.password !== void 0 && current.password !== "",
|
|
471
|
-
bypass: [...current.bypass],
|
|
468
|
+
enabled: current$1.enabled,
|
|
469
|
+
url: current$1.url,
|
|
470
|
+
...current$1.username === void 0 ? {} : { username: current$1.username },
|
|
471
|
+
passwordSet: current$1.password !== void 0 && current$1.password !== "",
|
|
472
|
+
bypass: [...current$1.bypass],
|
|
472
473
|
...configError === void 0 ? {} : { error: configError }
|
|
473
474
|
};
|
|
474
475
|
}
|
|
@@ -479,14 +480,14 @@ async function proxyGetConfig() {
|
|
|
479
480
|
* @returns the resulting view (secrets omitted).
|
|
480
481
|
*/
|
|
481
482
|
async function proxySetConfig(input) {
|
|
482
|
-
await ensureReady();
|
|
483
|
-
const password = input.password === void 0 ? current.password : input.password === null || input.password === "" ? void 0 : input.password;
|
|
483
|
+
await ensureReady$1();
|
|
484
|
+
const password = input.password === void 0 ? current$1.password : input.password === null || input.password === "" ? void 0 : input.password;
|
|
484
485
|
const next = normalizeConfig({
|
|
485
486
|
enabled: input.enabled,
|
|
486
487
|
url: input.url,
|
|
487
488
|
...input.username === void 0 ? {} : { username: input.username },
|
|
488
489
|
...password === void 0 ? {} : { password },
|
|
489
|
-
bypass: input.bypass ?? current.bypass
|
|
490
|
+
bypass: input.bypass ?? current$1.bypass
|
|
490
491
|
});
|
|
491
492
|
await persistConfig(next, proxyFilePath());
|
|
492
493
|
await applyConfig(next);
|
|
@@ -502,16 +503,16 @@ async function proxySetConfig(input) {
|
|
|
502
503
|
* host's global fetch.
|
|
503
504
|
*/
|
|
504
505
|
async function proxiedFetch(input, init = {}) {
|
|
505
|
-
await ensureReady();
|
|
506
|
+
await ensureReady$1();
|
|
506
507
|
let dispatcher;
|
|
507
|
-
if (current.enabled && agent !== void 0) {
|
|
508
|
+
if (current$1.enabled && agent !== void 0) {
|
|
508
509
|
let hostname = "";
|
|
509
510
|
try {
|
|
510
511
|
hostname = (typeof input === "string" ? new URL(input) : input instanceof URL ? input : new URL(input.url)).hostname;
|
|
511
512
|
} catch {
|
|
512
513
|
hostname = "";
|
|
513
514
|
}
|
|
514
|
-
if (!matchesBypass(hostname, current.bypass)) dispatcher = agent;
|
|
515
|
+
if (!matchesBypass(hostname, current$1.bypass)) dispatcher = agent;
|
|
515
516
|
}
|
|
516
517
|
if (dispatcher === void 0) return fetch(input, init);
|
|
517
518
|
return dispatchFetch(input, {
|
|
@@ -544,7 +545,7 @@ async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
|
|
|
544
545
|
error: errorMessage(error)
|
|
545
546
|
};
|
|
546
547
|
}
|
|
547
|
-
await ensureReady();
|
|
548
|
+
await ensureReady$1();
|
|
548
549
|
let probeAgent;
|
|
549
550
|
let viaProxy;
|
|
550
551
|
let closeProbe = false;
|
|
@@ -566,7 +567,7 @@ async function proxyTestConnection(target = DEFAULT_PROXY_TEST_URL, draft) {
|
|
|
566
567
|
};
|
|
567
568
|
}
|
|
568
569
|
else {
|
|
569
|
-
viaProxy = current.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current.bypass);
|
|
570
|
+
viaProxy = current$1.enabled && agent !== void 0 && !matchesBypass(parsed.hostname, current$1.bypass);
|
|
570
571
|
probeAgent = viaProxy ? agent : void 0;
|
|
571
572
|
}
|
|
572
573
|
const started = Date.now();
|
|
@@ -1227,6 +1228,22 @@ function readString(payload, field) {
|
|
|
1227
1228
|
if (typeof value !== "string" || value.length === 0) throw new BadRequest(`payload.${field} must be a non-empty string`);
|
|
1228
1229
|
return value;
|
|
1229
1230
|
}
|
|
1231
|
+
/** Validate the `setModelDefault` endpoint's payload. */
|
|
1232
|
+
function readModelDefaultInput(payload) {
|
|
1233
|
+
const provider = readProvider(payload);
|
|
1234
|
+
const model = readString(payload, "model");
|
|
1235
|
+
const record = payload;
|
|
1236
|
+
let effort;
|
|
1237
|
+
if (record.effort !== void 0) {
|
|
1238
|
+
if (typeof record.effort !== "string" || record.effort.length === 0) throw new BadRequest("payload.effort must be a non-empty string when present");
|
|
1239
|
+
effort = record.effort;
|
|
1240
|
+
}
|
|
1241
|
+
return {
|
|
1242
|
+
provider,
|
|
1243
|
+
model,
|
|
1244
|
+
...effort === void 0 ? {} : { effort }
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1230
1247
|
/** Validate the optional Claude login method. */
|
|
1231
1248
|
function readLoginMethod(payload, provider) {
|
|
1232
1249
|
const method = payload.method;
|
|
@@ -1279,6 +1296,14 @@ function readVideoName(payload) {
|
|
|
1279
1296
|
if (typeof name$1 !== "string" || !VIDEO_NAME_PATTERN.test(name$1)) throw new BadRequest("payload.name must be a bare .mp4 file name");
|
|
1280
1297
|
return name$1;
|
|
1281
1298
|
}
|
|
1299
|
+
/** Validate the `usage` endpoint's optional force flag. */
|
|
1300
|
+
function readForce(payload) {
|
|
1301
|
+
if (typeof payload !== "object" || payload === null) return false;
|
|
1302
|
+
const force = payload.force;
|
|
1303
|
+
if (force === void 0) return false;
|
|
1304
|
+
if (typeof force !== "boolean") throw new BadRequest("payload.force must be a boolean when present");
|
|
1305
|
+
return force;
|
|
1306
|
+
}
|
|
1282
1307
|
/** Validate the session id both speed endpoints carry. */
|
|
1283
1308
|
function readSessionId(payload) {
|
|
1284
1309
|
if (typeof payload !== "object" || payload === null) throw new BadRequest("payload must be an object");
|
|
@@ -1346,7 +1371,7 @@ function readProxyTestPayload(payload) {
|
|
|
1346
1371
|
...proxy === void 0 ? {} : { proxy }
|
|
1347
1372
|
};
|
|
1348
1373
|
}
|
|
1349
|
-
async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
1374
|
+
async function dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal) {
|
|
1350
1375
|
switch (endpoint) {
|
|
1351
1376
|
case "status": {
|
|
1352
1377
|
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
|
|
@@ -1376,7 +1401,7 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
1376
1401
|
}
|
|
1377
1402
|
case "usage": {
|
|
1378
1403
|
const provider = readProvider(payload);
|
|
1379
|
-
return ok(await controller.usage(provider, readString(payload, "account"), signal));
|
|
1404
|
+
return ok(await controller.usage(provider, readString(payload, "account"), signal, readForce(payload)));
|
|
1380
1405
|
}
|
|
1381
1406
|
case "image": return ok(await controller.readImage(readImageRef(payload), signal));
|
|
1382
1407
|
case "video": return ok(await controller.readVideo(readVideoName(payload), signal));
|
|
@@ -1393,6 +1418,16 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
1393
1418
|
case "proxyTest":
|
|
1394
1419
|
if (proxy === void 0) throw new BadRequest("proxy configuration is unavailable");
|
|
1395
1420
|
return ok(await proxy.test(readProxyTestPayload(payload)));
|
|
1421
|
+
case "modelDefaults":
|
|
1422
|
+
if (modelDefaults === void 0) throw new BadRequest("model defaults are unavailable");
|
|
1423
|
+
return ok(await modelDefaults.catalog());
|
|
1424
|
+
case "setModelDefault":
|
|
1425
|
+
if (modelDefaults === void 0) throw new BadRequest("model defaults are unavailable");
|
|
1426
|
+
{
|
|
1427
|
+
const input = readModelDefaultInput(payload);
|
|
1428
|
+
await modelDefaults.set(input.provider, input.model, input.effort);
|
|
1429
|
+
}
|
|
1430
|
+
return ok({ ok: true });
|
|
1396
1431
|
default: throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
1397
1432
|
}
|
|
1398
1433
|
}
|
|
@@ -1402,13 +1437,14 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
1402
1437
|
* @param controller - the auth operations backing the endpoints.
|
|
1403
1438
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
1404
1439
|
* @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
|
|
1440
|
+
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
1405
1441
|
*/
|
|
1406
|
-
function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
|
|
1442
|
+
function registerAuthRpc(ctx, controller, speed, proxy = void 0, modelDefaults = void 0) {
|
|
1407
1443
|
ctx.inject(["connection"], (ctx$1) => {
|
|
1408
1444
|
const connection = ctx$1.get("connection");
|
|
1409
1445
|
ctx$1.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
|
|
1410
1446
|
try {
|
|
1411
|
-
return await dispatch(controller, speed, proxy, endpoint, payload, signal);
|
|
1447
|
+
return await dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal);
|
|
1412
1448
|
} catch (error) {
|
|
1413
1449
|
return failure(error);
|
|
1414
1450
|
}
|
|
@@ -1416,6 +1452,442 @@ function registerAuthRpc(ctx, controller, speed, proxy = void 0) {
|
|
|
1416
1452
|
});
|
|
1417
1453
|
}
|
|
1418
1454
|
|
|
1455
|
+
//#endregion
|
|
1456
|
+
//#region src/model-defaults.ts
|
|
1457
|
+
/** Absolute path of the defaults file. */
|
|
1458
|
+
function modelDefaultsFilePath() {
|
|
1459
|
+
return dshHomePath("plugins", "subscriptions", "model-defaults.json");
|
|
1460
|
+
}
|
|
1461
|
+
const EMPTY = Object.freeze({});
|
|
1462
|
+
/** In-memory snapshot read by every consumer (adapters, RPC). */
|
|
1463
|
+
let current = EMPTY;
|
|
1464
|
+
/** One lazy load of the on-disk file (read once per process). */
|
|
1465
|
+
let ready;
|
|
1466
|
+
/** Last load failure, surfaced to callers that care; defaults stay empty. */
|
|
1467
|
+
let loadError;
|
|
1468
|
+
/**
|
|
1469
|
+
* Serialises every write: the read-modify-write sequence must not interleave,
|
|
1470
|
+
* or a fast second save would compute its snapshot from the stale `current`
|
|
1471
|
+
* and silently drop the first update (the UI disables only the row being
|
|
1472
|
+
* saved, so overlaps are reachable).
|
|
1473
|
+
*/
|
|
1474
|
+
let writeChain = Promise.resolve();
|
|
1475
|
+
/**
|
|
1476
|
+
* Validate one persisted provider section: a string→string map, or undefined.
|
|
1477
|
+
* Malformed *entries* are skipped, not the whole section: one bad value (a
|
|
1478
|
+
* hand edit losing its quotes) must not silently un-configure every model in
|
|
1479
|
+
* that provider. What was dropped is reported so the caller can surface it
|
|
1480
|
+
* instead of the loss disappearing.
|
|
1481
|
+
*/
|
|
1482
|
+
function sanitizeProvider(value, dropped) {
|
|
1483
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1484
|
+
const entries = {};
|
|
1485
|
+
for (const [model, effort] of Object.entries(value)) {
|
|
1486
|
+
if (typeof effort !== "string" || effort.length === 0) {
|
|
1487
|
+
dropped.push(model);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
entries[model] = effort;
|
|
1491
|
+
}
|
|
1492
|
+
if (Object.keys(entries).length === 0) return void 0;
|
|
1493
|
+
return Object.freeze(entries);
|
|
1494
|
+
}
|
|
1495
|
+
/** Validate the raw document: only known providers, malformed sections dropped. */
|
|
1496
|
+
function sanitizeDefaults(value) {
|
|
1497
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {
|
|
1498
|
+
defaults: EMPTY,
|
|
1499
|
+
dropped: []
|
|
1500
|
+
};
|
|
1501
|
+
const record = value;
|
|
1502
|
+
const result = {};
|
|
1503
|
+
const dropped = [];
|
|
1504
|
+
for (const provider of PROVIDER_IDS) {
|
|
1505
|
+
const section = sanitizeProvider(record[provider], dropped);
|
|
1506
|
+
if (section !== void 0) result[provider] = section;
|
|
1507
|
+
}
|
|
1508
|
+
return {
|
|
1509
|
+
defaults: Object.freeze(result),
|
|
1510
|
+
dropped
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
/** Read and validate the on-disk file; a missing file reads as empty. */
|
|
1514
|
+
async function loadFile(path) {
|
|
1515
|
+
let text;
|
|
1516
|
+
try {
|
|
1517
|
+
text = await readFile(path, "utf8");
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
if (error.code === "ENOENT") return EMPTY;
|
|
1520
|
+
throw error;
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const { defaults, dropped } = sanitizeDefaults(JSON.parse(text));
|
|
1524
|
+
if (dropped.length > 0) loadError = /* @__PURE__ */ new Error(`subscriptions model defaults: ${dropped.length} malformed entr${dropped.length === 1 ? "y" : "ies"} skipped (${dropped.join(", ")}); fix or delete the file`);
|
|
1525
|
+
return defaults;
|
|
1526
|
+
} catch {
|
|
1527
|
+
throw new Error(`subscriptions model defaults at ${path} are not valid JSON; fix or delete the file`);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
/** Resolve the module state once from disk; failures leave the defaults empty. */
|
|
1531
|
+
async function ensureReady() {
|
|
1532
|
+
ready ??= loadFile(modelDefaultsFilePath()).then((loaded) => {
|
|
1533
|
+
current = loaded;
|
|
1534
|
+
}, (error) => {
|
|
1535
|
+
loadError = error;
|
|
1536
|
+
current = EMPTY;
|
|
1537
|
+
});
|
|
1538
|
+
return ready;
|
|
1539
|
+
}
|
|
1540
|
+
/** Persist a snapshot atomically with owner-only permissions. */
|
|
1541
|
+
async function atomicPersist(defaults, path) {
|
|
1542
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1543
|
+
const tmp = `${path}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
1544
|
+
try {
|
|
1545
|
+
await writeFile(tmp, JSON.stringify(defaults, null, 2), { mode: 384 });
|
|
1546
|
+
await chmod(tmp, 384);
|
|
1547
|
+
await rename(tmp, path);
|
|
1548
|
+
} catch (error) {
|
|
1549
|
+
await rm(tmp, { force: true });
|
|
1550
|
+
throw error;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
let persistDefaults = atomicPersist;
|
|
1554
|
+
/**
|
|
1555
|
+
* Clone one provider section, or undefined when nothing is configured for it.
|
|
1556
|
+
* The clone is prototype-less: model ids are provider-supplied catalog data
|
|
1557
|
+
* used as object keys, and consumers index the section directly (the RPC
|
|
1558
|
+
* catalog in index.ts does), so an id like `toString` would otherwise yield an
|
|
1559
|
+
* inherited *function* where a string is declared.
|
|
1560
|
+
*/
|
|
1561
|
+
function sectionOf(defaults, provider) {
|
|
1562
|
+
const section = defaults[provider];
|
|
1563
|
+
if (section === void 0) return void 0;
|
|
1564
|
+
return Object.assign(Object.create(null), section);
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Ready the defaults store.
|
|
1568
|
+
* @internal Exported for tests; index.ts calls it at apply time so every
|
|
1569
|
+
* later synchronous read sees the persisted state.
|
|
1570
|
+
*/
|
|
1571
|
+
async function loadModelDefaults() {
|
|
1572
|
+
await ensureReady();
|
|
1573
|
+
}
|
|
1574
|
+
/**
|
|
1575
|
+
* The configured default effort for one model, or undefined when none (the
|
|
1576
|
+
* picker then follows the provider's own default).
|
|
1577
|
+
* @internal Exported for the adapters' `defaultEffortOf` options.
|
|
1578
|
+
*/
|
|
1579
|
+
function defaultEffortOf(provider, model) {
|
|
1580
|
+
const section = current[provider];
|
|
1581
|
+
if (section === void 0) return void 0;
|
|
1582
|
+
return Object.prototype.hasOwnProperty.call(section, model) ? section[model] : void 0;
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Set or clear one model's configured default effort, then persist. The
|
|
1586
|
+
* memory snapshot updates only after the atomic write succeeds, so a failed
|
|
1587
|
+
* write never leaves the live state ahead of the file.
|
|
1588
|
+
* @param provider - the subscription provider route.
|
|
1589
|
+
* @param model - the wire model id.
|
|
1590
|
+
* @param effort - the effort id, or undefined to clear the override.
|
|
1591
|
+
*/
|
|
1592
|
+
function setDefaultEffort(provider, model, effort) {
|
|
1593
|
+
const run = writeChain.then(async () => {
|
|
1594
|
+
await ensureReady();
|
|
1595
|
+
const section = { ...sectionOf(current, provider) ?? {} };
|
|
1596
|
+
if (effort === void 0) delete section[model];
|
|
1597
|
+
else section[model] = effort;
|
|
1598
|
+
const next = { ...current };
|
|
1599
|
+
if (Object.keys(section).length === 0) delete next[provider];
|
|
1600
|
+
else next[provider] = Object.freeze(section);
|
|
1601
|
+
const frozen = Object.freeze(next);
|
|
1602
|
+
await persistDefaults(frozen, modelDefaultsFilePath());
|
|
1603
|
+
current = frozen;
|
|
1604
|
+
});
|
|
1605
|
+
writeChain = run.catch(() => void 0);
|
|
1606
|
+
return run;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
//#endregion
|
|
1610
|
+
//#region src/providers/rate-limit.ts
|
|
1611
|
+
/**
|
|
1612
|
+
* Extra time added to every provider-disclosed wait. Absorbs clock skew
|
|
1613
|
+
* between the harness and the provider, so a retry does not land a moment
|
|
1614
|
+
* before the window actually reopens and burn an attempt on a second 429.
|
|
1615
|
+
*/
|
|
1616
|
+
const RESET_GRACE_MS = 2e3;
|
|
1617
|
+
/** Shortest wait ever scheduled, including for a reset instant already in the past. */
|
|
1618
|
+
const MIN_WAIT_MS = 1e3;
|
|
1619
|
+
/** Below this a bare number is a delay in seconds rather than an epoch stamp. */
|
|
1620
|
+
const EPOCH_SECONDS_FLOOR = 1e9;
|
|
1621
|
+
/** At or above this a bare epoch stamp is already in milliseconds. */
|
|
1622
|
+
const EPOCH_MILLIS_FLOOR = 0xe8d4a51000;
|
|
1623
|
+
/** Node's maximum timer delay; a longer wait cannot be scheduled at all. */
|
|
1624
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
1625
|
+
/** Default ceiling on a rate-limit wait: six hours covers a five-hour session window with slack. */
|
|
1626
|
+
const DEFAULT_RATE_LIMIT_MAX_WAIT_MS = 360 * 60 * 1e3;
|
|
1627
|
+
/**
|
|
1628
|
+
* Interpret a bare numeric rate-limit value, which providers write in three
|
|
1629
|
+
* shapes: epoch milliseconds, epoch seconds, or a delay in seconds. The
|
|
1630
|
+
* magnitude separates them unambiguously for any plausible value — an epoch in
|
|
1631
|
+
* seconds is ~1.8e9 today, while a delay of even a full week is ~6e5.
|
|
1632
|
+
* @param value - the raw numeric value.
|
|
1633
|
+
* @param now - the current epoch milliseconds.
|
|
1634
|
+
* @returns epoch milliseconds of the reset, or undefined when the value is unusable.
|
|
1635
|
+
*/
|
|
1636
|
+
function resetInstantFromNumber(value, now) {
|
|
1637
|
+
if (!Number.isFinite(value) || value <= 0) return void 0;
|
|
1638
|
+
if (value >= EPOCH_MILLIS_FLOOR) return value;
|
|
1639
|
+
if (value >= EPOCH_SECONDS_FLOOR) return value * 1e3;
|
|
1640
|
+
return now + value * 1e3;
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Parse a Go-style duration (`6m0s`, `1h2m3.5s`, `150ms`) into milliseconds —
|
|
1644
|
+
* the form OpenAI-compatible `x-ratelimit-reset-*` headers use.
|
|
1645
|
+
* @param text - the raw header value.
|
|
1646
|
+
* @returns the duration in milliseconds, or undefined when the text is not one.
|
|
1647
|
+
*/
|
|
1648
|
+
function durationMs(text) {
|
|
1649
|
+
const trimmed = text.trim();
|
|
1650
|
+
if (trimmed.length === 0) return void 0;
|
|
1651
|
+
const pattern = /(\d+(?:\.\d+)?)(ms|h|m|s)/y;
|
|
1652
|
+
const units = {
|
|
1653
|
+
h: 36e5,
|
|
1654
|
+
m: 6e4,
|
|
1655
|
+
s: 1e3,
|
|
1656
|
+
ms: 1
|
|
1657
|
+
};
|
|
1658
|
+
let total = 0;
|
|
1659
|
+
let matched = false;
|
|
1660
|
+
let index = 0;
|
|
1661
|
+
let previousUnit = Number.POSITIVE_INFINITY;
|
|
1662
|
+
for (;;) {
|
|
1663
|
+
pattern.lastIndex = index;
|
|
1664
|
+
const match = pattern.exec(trimmed);
|
|
1665
|
+
if (match === null) break;
|
|
1666
|
+
const unit = units[match[2]];
|
|
1667
|
+
if (unit >= previousUnit) return void 0;
|
|
1668
|
+
previousUnit = unit;
|
|
1669
|
+
total += Number(match[1]) * unit;
|
|
1670
|
+
index = pattern.lastIndex;
|
|
1671
|
+
matched = true;
|
|
1672
|
+
}
|
|
1673
|
+
if (!matched || index !== trimmed.length) return void 0;
|
|
1674
|
+
return total > 0 ? total : void 0;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Interpret any single rate-limit value — a number, a numeric string, a
|
|
1678
|
+
* duration (`6m0s`), or a date — as the instant a window reopens. One reader
|
|
1679
|
+
* for every shape, so a provider that changes the encoding of a field it
|
|
1680
|
+
* already sends does not need a code change here.
|
|
1681
|
+
* @param value - the raw header value or JSON field.
|
|
1682
|
+
* @param now - the current epoch milliseconds.
|
|
1683
|
+
* @returns epoch milliseconds of the reset, or undefined when the value is unusable.
|
|
1684
|
+
*/
|
|
1685
|
+
function resetInstantFromValue(value, now) {
|
|
1686
|
+
if (typeof value === "number") return resetInstantFromNumber(value, now);
|
|
1687
|
+
if (typeof value !== "string") return void 0;
|
|
1688
|
+
const trimmed = value.trim();
|
|
1689
|
+
if (trimmed.length === 0) return void 0;
|
|
1690
|
+
const numeric = Number(trimmed);
|
|
1691
|
+
if (Number.isFinite(numeric)) return resetInstantFromNumber(numeric, now);
|
|
1692
|
+
const duration = durationMs(trimmed);
|
|
1693
|
+
if (duration !== void 0) return now + duration;
|
|
1694
|
+
const parsed = Date.parse(trimmed);
|
|
1695
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Read a header carrying any of the {@link resetInstantFromValue} shapes.
|
|
1699
|
+
* @param response - the failed response.
|
|
1700
|
+
* @param name - the header to read.
|
|
1701
|
+
* @param now - the current epoch milliseconds.
|
|
1702
|
+
* @returns epoch milliseconds of the reset, or undefined when absent or unusable.
|
|
1703
|
+
*/
|
|
1704
|
+
function resetInstantFromHeader(response, name$1, now) {
|
|
1705
|
+
return resetInstantFromValue(response.headers.get(name$1), now);
|
|
1706
|
+
}
|
|
1707
|
+
/**
|
|
1708
|
+
* Read the RFC 7231 `retry-after` header in both its forms: a delay in seconds
|
|
1709
|
+
* (never an epoch stamp, whatever its magnitude) or an HTTP-date.
|
|
1710
|
+
* @param response - the failed response.
|
|
1711
|
+
* @param now - the current epoch milliseconds.
|
|
1712
|
+
* @returns epoch milliseconds of the reset, or undefined when absent or unusable.
|
|
1713
|
+
*/
|
|
1714
|
+
function retryAfterInstant(response, now) {
|
|
1715
|
+
const raw = response.headers.get("retry-after");
|
|
1716
|
+
if (raw === null) return void 0;
|
|
1717
|
+
const trimmed = raw.trim();
|
|
1718
|
+
if (trimmed.length === 0) return void 0;
|
|
1719
|
+
const seconds = Number(trimmed);
|
|
1720
|
+
if (Number.isFinite(seconds)) return seconds > 0 ? now + seconds * 1e3 : void 0;
|
|
1721
|
+
const parsed = Date.parse(trimmed);
|
|
1722
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Parse a response body as JSON without throwing on the non-JSON bodies
|
|
1726
|
+
* providers occasionally return under load (an HTML gateway page, say).
|
|
1727
|
+
* @param body - the complete response body.
|
|
1728
|
+
* @returns the parsed value, or undefined when the body is not JSON.
|
|
1729
|
+
*/
|
|
1730
|
+
function jsonBody(body) {
|
|
1731
|
+
if (body.length === 0) return void 0;
|
|
1732
|
+
try {
|
|
1733
|
+
return JSON.parse(body);
|
|
1734
|
+
} catch {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
/** How deep {@link resetFromFields} walks; every observed payload nests one or two levels. */
|
|
1739
|
+
const MAX_BODY_DEPTH = 4;
|
|
1740
|
+
/**
|
|
1741
|
+
* Find a reset instant under any of the named keys, anywhere in a parsed body.
|
|
1742
|
+
*
|
|
1743
|
+
* The search is by key rather than by path on purpose: providers move the same
|
|
1744
|
+
* field between containers (`detail`, `error`, top level) across endpoints and
|
|
1745
|
+
* versions, and a path-shaped reader silently stops working when they do. Only
|
|
1746
|
+
* the key list is provider-specific.
|
|
1747
|
+
* @param value - the parsed body, or any nested value.
|
|
1748
|
+
* @param keys - field names this provider uses for a reset or delay.
|
|
1749
|
+
* @param now - the current epoch milliseconds.
|
|
1750
|
+
* @param depth - remaining recursion depth.
|
|
1751
|
+
* @returns the earliest instant found, or undefined when no key matched.
|
|
1752
|
+
*/
|
|
1753
|
+
function resetFromFields(value, keys, now, depth = MAX_BODY_DEPTH) {
|
|
1754
|
+
if (depth <= 0 || value === null || typeof value !== "object") return void 0;
|
|
1755
|
+
let earliest;
|
|
1756
|
+
const consider = (candidate) => {
|
|
1757
|
+
if (candidate !== void 0 && (earliest === void 0 || candidate < earliest)) earliest = candidate;
|
|
1758
|
+
};
|
|
1759
|
+
if (Array.isArray(value)) {
|
|
1760
|
+
for (const item of value) consider(resetFromFields(item, keys, now, depth - 1));
|
|
1761
|
+
return earliest;
|
|
1762
|
+
}
|
|
1763
|
+
for (const [key, nested] of Object.entries(value)) if (keys.includes(key)) consider(resetInstantFromValue(nested, now));
|
|
1764
|
+
else consider(resetFromFields(nested, keys, now, depth - 1));
|
|
1765
|
+
return earliest;
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* The earliest of several candidate reset instants, ignoring absent ones. The
|
|
1769
|
+
* earliest is the one that matters: it is the first moment any of the reported
|
|
1770
|
+
* limits allows a request again.
|
|
1771
|
+
* @param candidates - reset instants in no particular order.
|
|
1772
|
+
* @returns the earliest instant, or undefined when every candidate is absent.
|
|
1773
|
+
*/
|
|
1774
|
+
function earliestReset(...candidates) {
|
|
1775
|
+
let earliest;
|
|
1776
|
+
for (const candidate of candidates) {
|
|
1777
|
+
if (candidate === void 0) continue;
|
|
1778
|
+
if (earliest === void 0 || candidate < earliest) earliest = candidate;
|
|
1779
|
+
}
|
|
1780
|
+
return earliest;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Turn a reset instant into the wait to report as `providerRetryAfterMs`.
|
|
1784
|
+
*
|
|
1785
|
+
* Deliberately not capped: a reset beyond the policy's `maxDelayMs` makes the
|
|
1786
|
+
* retry plugin delegate immediately, failing the turn at once with the real
|
|
1787
|
+
* reset in the message, rather than clamping the wait down and burning the
|
|
1788
|
+
* retry budget against a window that is still closed.
|
|
1789
|
+
* @param instant - epoch milliseconds the window reopens.
|
|
1790
|
+
* @param now - the current epoch milliseconds.
|
|
1791
|
+
* @returns the wait in milliseconds, never below {@link MIN_WAIT_MS}.
|
|
1792
|
+
*/
|
|
1793
|
+
function waitFromReset(instant, now) {
|
|
1794
|
+
return Math.max(MIN_WAIT_MS, instant - now + RESET_GRACE_MS);
|
|
1795
|
+
}
|
|
1796
|
+
/** Header names worth showing when a 429 disclosed no reset this code recognizes. */
|
|
1797
|
+
const DIAGNOSTIC_HEADER = /rate-?limit|retry|reset|^x-codex-/i;
|
|
1798
|
+
/**
|
|
1799
|
+
* Render the rate-limit-shaped headers and the head of the body of a 429 whose
|
|
1800
|
+
* reset instant nothing parsed. Emitted through the adapter's `onWarn`, this is
|
|
1801
|
+
* how an unrecognized provider field gets named from live traffic instead of
|
|
1802
|
+
* being guessed at.
|
|
1803
|
+
*
|
|
1804
|
+
* It is also where the per-bucket rollover snapshots land by design — no reader
|
|
1805
|
+
* parks a turn on one, because on a 429 they cannot say which bucket refused —
|
|
1806
|
+
* so the operator still sees what the provider disclosed.
|
|
1807
|
+
* @param response - the failed response.
|
|
1808
|
+
* @param body - the complete response body.
|
|
1809
|
+
* @returns a one-line diagnostic.
|
|
1810
|
+
*/
|
|
1811
|
+
function rateLimitDiagnostics(response, body) {
|
|
1812
|
+
const headers = [];
|
|
1813
|
+
response.headers.forEach((value, key) => {
|
|
1814
|
+
if (DIAGNOSTIC_HEADER.test(key)) headers.push(`${key}: ${value}`);
|
|
1815
|
+
});
|
|
1816
|
+
headers.sort();
|
|
1817
|
+
const rendered = headers.length > 0 ? headers.join("; ") : "(none)";
|
|
1818
|
+
const head = body.slice(0, 200);
|
|
1819
|
+
return `429 disclosed no reset time; headers [${rendered}]; body ${head.length > 0 ? head : "(empty)"}`;
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* The retry shape every subscription route starts from: Claude Code's own SDK
|
|
1823
|
+
* numbers — ten retries after the first attempt, exponential backoff from 1s
|
|
1824
|
+
* doubling per attempt, capped at 60s, plus 20% jitter.
|
|
1825
|
+
*
|
|
1826
|
+
* Shared across all four routes rather than kept to claude, because what these
|
|
1827
|
+
* numbers are tuned for is the shape of a subscription endpoint — a consumer
|
|
1828
|
+
* plan behind a session window, which sheds load in bursts and rewards an
|
|
1829
|
+
* attempt that outlasts them — and that is the same on all four. The dsh-llm
|
|
1830
|
+
* defaults (5 retries from 500ms to 10s) give up after about fifteen seconds,
|
|
1831
|
+
* which is short for that.
|
|
1832
|
+
*
|
|
1833
|
+
* The 60s cap governs local backoff only: a disclosed rate-limit reset is
|
|
1834
|
+
* accepted up to the configured wait ceiling instead.
|
|
1835
|
+
*/
|
|
1836
|
+
const DEFAULT_RETRY = Object.freeze({
|
|
1837
|
+
maxRetries: 10,
|
|
1838
|
+
initialDelayMs: 1e3,
|
|
1839
|
+
maxDelayMs: 6e4,
|
|
1840
|
+
jitterRatio: .2
|
|
1841
|
+
});
|
|
1842
|
+
/** Waiting behavior a route falls back to when the plugin passed none (waiting on, six-hour ceiling). */
|
|
1843
|
+
const DEFAULT_RATE_LIMIT_WAIT = Object.freeze({
|
|
1844
|
+
wait: true,
|
|
1845
|
+
maxWaitMs: DEFAULT_RATE_LIMIT_MAX_WAIT_MS
|
|
1846
|
+
});
|
|
1847
|
+
/**
|
|
1848
|
+
* Validate and default the rate-limit waiting config.
|
|
1849
|
+
* @param config - the raw plugin config section, when present.
|
|
1850
|
+
* @param path - diagnostic path naming the config that owns the value.
|
|
1851
|
+
* @returns the resolved, immutable behavior.
|
|
1852
|
+
*/
|
|
1853
|
+
function resolveRateLimitWait(config, path) {
|
|
1854
|
+
const wait = config?.wait ?? true;
|
|
1855
|
+
const maxWaitMs = config?.maxWaitMs ?? DEFAULT_RATE_LIMIT_MAX_WAIT_MS;
|
|
1856
|
+
if (!Number.isFinite(maxWaitMs) || maxWaitMs <= 0) throw new Error(`${path}.maxWaitMs must be a positive finite number of milliseconds`);
|
|
1857
|
+
if (maxWaitMs > MAX_TIMER_DELAY_MS) throw new Error(`${path}.maxWaitMs must be no greater than ${String(MAX_TIMER_DELAY_MS)} (the maximum schedulable delay)`);
|
|
1858
|
+
return Object.freeze({
|
|
1859
|
+
wait,
|
|
1860
|
+
maxWaitMs
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Resolve one route's retry policy, widening the delay ceiling to the
|
|
1865
|
+
* configured wait so a disclosed reset hours out is accepted rather than
|
|
1866
|
+
* refused.
|
|
1867
|
+
*
|
|
1868
|
+
* The ceiling is shared with local exponential backoff, so widening it also
|
|
1869
|
+
* raises how long an unrelated transient failure may back off for. That stays
|
|
1870
|
+
* bounded by the finite retry budget — the claude route's ten retries reach
|
|
1871
|
+
* 512 s per attempt at most — and it only governs when the provider disclosed
|
|
1872
|
+
* nothing, which is exactly the case where a longer wait is the safer guess.
|
|
1873
|
+
* @param defaults - the route's retry shape.
|
|
1874
|
+
* @param rateLimit - resolved waiting behavior.
|
|
1875
|
+
* @param path - diagnostic path naming the provider route.
|
|
1876
|
+
* @returns the policy to report from `providerRetryPolicy`.
|
|
1877
|
+
*/
|
|
1878
|
+
function subscriptionRetryPolicy(defaults, rateLimit, path) {
|
|
1879
|
+
const maxDelayMs = rateLimit.wait ? Math.max(defaults.maxDelayMs, rateLimit.maxWaitMs) : defaults.maxDelayMs;
|
|
1880
|
+
return resolveRetryPolicy({
|
|
1881
|
+
mode: "normal",
|
|
1882
|
+
maxRetries: defaults.maxRetries,
|
|
1883
|
+
backoff: {
|
|
1884
|
+
initialDelayMs: defaults.initialDelayMs,
|
|
1885
|
+
maxDelayMs,
|
|
1886
|
+
jitterRatio: defaults.jitterRatio
|
|
1887
|
+
}
|
|
1888
|
+
}, path);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1419
1891
|
//#endregion
|
|
1420
1892
|
//#region src/providers/common.ts
|
|
1421
1893
|
/**
|
|
@@ -1446,38 +1918,56 @@ function validateModels(models, label) {
|
|
|
1446
1918
|
});
|
|
1447
1919
|
}
|
|
1448
1920
|
/**
|
|
1449
|
-
* Build an LlmError from a non-2xx provider response,
|
|
1450
|
-
*
|
|
1921
|
+
* Build an LlmError from a non-2xx provider response, mapping the status to a
|
|
1922
|
+
* stable code and, for a rate-limited request, the disclosed reset instant to
|
|
1923
|
+
* the `providerRetryAfterMs` the retry plugin waits out.
|
|
1924
|
+
*
|
|
1925
|
+
* A 429 classifies as `RATE_LIMIT` on the strength of the status alone, ahead
|
|
1926
|
+
* of the quota-wording check. On these routes there is no terminal quota to
|
|
1927
|
+
* distinguish: a subscription has no balance to top up, only a window that
|
|
1928
|
+
* reopens, and providers announce an exhausted window with wording
|
|
1929
|
+
* (`usage_limit_reached`) the shared classifier reads as permanent.
|
|
1451
1930
|
* @param response - the failed response.
|
|
1452
1931
|
* @param label - diagnostic prefix naming the provider API.
|
|
1932
|
+
* @param options - the calling provider's rate-limit reader and warning sink.
|
|
1453
1933
|
* @returns the classified error.
|
|
1454
1934
|
*/
|
|
1455
|
-
async function httpLlmError(response, label) {
|
|
1935
|
+
async function httpLlmError(response, label, options = {}) {
|
|
1456
1936
|
let body = "";
|
|
1457
1937
|
try {
|
|
1458
|
-
body =
|
|
1938
|
+
body = await response.text();
|
|
1459
1939
|
} catch {}
|
|
1460
|
-
const
|
|
1940
|
+
const shown = body.slice(0, 500);
|
|
1941
|
+
const message = shown.length > 0 ? `${label} error (HTTP ${String(response.status)}): ${shown}` : `${label} error (HTTP ${String(response.status)})`;
|
|
1461
1942
|
let code;
|
|
1462
1943
|
if (response.status === 401 || response.status === 403) code = "AUTH";
|
|
1463
|
-
else if (isQuotaExceededError(body)) code = QUOTA_EXCEEDED_CODE;
|
|
1464
1944
|
else if (response.status === 429) code = "RATE_LIMIT";
|
|
1465
|
-
else if (
|
|
1945
|
+
else if (isQuotaExceededError(shown)) code = QUOTA_EXCEEDED_CODE;
|
|
1946
|
+
else if (response.status === 400 && isContextWindowExceededError(shown)) code = CONTEXT_WINDOW_EXCEEDED_CODE;
|
|
1466
1947
|
else if (response.status === 408 || response.status === 504) code = "TIMEOUT";
|
|
1467
1948
|
else if (response.status >= 500) code = "SERVER";
|
|
1468
1949
|
else code = `HTTP_${String(response.status)}`;
|
|
1469
|
-
const
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
if (Number.isFinite(seconds) && seconds > 0) providerRetryAfterMs = seconds * 1e3;
|
|
1474
|
-
}
|
|
1950
|
+
const now = Date.now();
|
|
1951
|
+
const rateLimited = response.status === 429;
|
|
1952
|
+
const reset = rateLimited ? options.rateLimitReset?.(response, body, now) ?? retryAfterInstant(response, now) : retryAfterInstant(response, now);
|
|
1953
|
+
if (reset === void 0 && rateLimited) options.onWarn?.(`${label}: ${rateLimitDiagnostics(response, body)}`);
|
|
1475
1954
|
return new LlmError(message, code, {
|
|
1476
1955
|
status: response.status,
|
|
1477
|
-
...
|
|
1956
|
+
...reset === void 0 ? {} : { providerRetryAfterMs: waitFromReset(reset, now) }
|
|
1478
1957
|
});
|
|
1479
1958
|
}
|
|
1480
1959
|
/**
|
|
1960
|
+
* Parse a response's `retry-after` header (seconds) into milliseconds.
|
|
1961
|
+
* @param response - the failed response.
|
|
1962
|
+
* @returns the delay in ms, or undefined when absent/unusable.
|
|
1963
|
+
*/
|
|
1964
|
+
function parseRetryAfterMs(response) {
|
|
1965
|
+
const retryAfter = response.headers.get("retry-after");
|
|
1966
|
+
if (retryAfter === null) return void 0;
|
|
1967
|
+
const seconds = Number(retryAfter);
|
|
1968
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
1969
|
+
}
|
|
1970
|
+
/**
|
|
1481
1971
|
* Create an idle watchdog chained to the caller's signal.
|
|
1482
1972
|
* @param caller - the request's own abort signal, when present.
|
|
1483
1973
|
* @param timeoutMs - maximum idle interval while a stream read is outstanding.
|
|
@@ -1531,11 +2021,20 @@ var OAuthEndpointError = class extends Error {
|
|
|
1531
2021
|
status;
|
|
1532
2022
|
/** The provider's OAuth `error` code (e.g. `invalid_grant`), when present. */
|
|
1533
2023
|
oauthCode;
|
|
1534
|
-
|
|
2024
|
+
/**
|
|
2025
|
+
* The endpoint's `retry-after`, in ms, when it sent one. Usage/models
|
|
2026
|
+
* endpoints reuse this error type and can rate-limit progressively (each
|
|
2027
|
+
* hit within the window extends the next one), so a caller retrying on a
|
|
2028
|
+
* fixed schedule instead of honoring this can keep an account locked out
|
|
2029
|
+
* indefinitely.
|
|
2030
|
+
*/
|
|
2031
|
+
retryAfterMs;
|
|
2032
|
+
constructor(message, status, oauthCode, retryAfterMs$1) {
|
|
1535
2033
|
super(message);
|
|
1536
2034
|
this.name = "OAuthEndpointError";
|
|
1537
2035
|
this.status = status;
|
|
1538
2036
|
this.oauthCode = oauthCode;
|
|
2037
|
+
this.retryAfterMs = retryAfterMs$1;
|
|
1539
2038
|
}
|
|
1540
2039
|
};
|
|
1541
2040
|
/**
|
|
@@ -1552,7 +2051,7 @@ async function oauthEndpointError(response, label) {
|
|
|
1552
2051
|
oauthCode = typeof parsed.error === "string" ? parsed.error : void 0;
|
|
1553
2052
|
detail = typeof parsed.error_description === "string" ? parsed.error_description : oauthCode ?? "";
|
|
1554
2053
|
} catch {}
|
|
1555
|
-
return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode);
|
|
2054
|
+
return new OAuthEndpointError(detail.length > 0 ? `${label} token endpoint error (HTTP ${String(response.status)}): ${detail}` : `${label} token endpoint error (HTTP ${String(response.status)})`, response.status, oauthCode, parseRetryAfterMs(response));
|
|
1556
2055
|
}
|
|
1557
2056
|
/**
|
|
1558
2057
|
* Per-provider session freshness: loads the stored session, refreshes
|
|
@@ -1609,9 +2108,9 @@ var TokenManager = class {
|
|
|
1609
2108
|
}
|
|
1610
2109
|
}
|
|
1611
2110
|
async doRefresh(session) {
|
|
1612
|
-
const current$
|
|
1613
|
-
if (current$
|
|
1614
|
-
const next = await this.options.refresh(current$
|
|
2111
|
+
const current$2 = await this.options.load();
|
|
2112
|
+
if (current$2 !== void 0 && current$2.accessToken !== session.accessToken && current$2.expiresAt - Date.now() > this.options.preemptMs) return current$2;
|
|
2113
|
+
const next = await this.options.refresh(current$2 ?? session);
|
|
1615
2114
|
await this.options.save(next);
|
|
1616
2115
|
return next;
|
|
1617
2116
|
}
|
|
@@ -1633,6 +2132,59 @@ function withTimeout(work, timeoutMs) {
|
|
|
1633
2132
|
throw error;
|
|
1634
2133
|
}), aborted]);
|
|
1635
2134
|
}
|
|
2135
|
+
/** Display name for a wire reasoning-effort identifier. */
|
|
2136
|
+
function effortDisplayName(effort) {
|
|
2137
|
+
return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Fold a configured per-model default effort into a reasoning block, keeping
|
|
2141
|
+
* the DSH runtime invariant `defaultEffort ∈ efforts` (the runtime rejects an
|
|
2142
|
+
* unknown default with `INVALID_MODEL_REASONING`).
|
|
2143
|
+
*
|
|
2144
|
+
* A configured level the base set does not advertise is *dropped*, not
|
|
2145
|
+
* appended: for claude/grok/copilot the base is the provider's live catalog,
|
|
2146
|
+
* i.e. the truth about what the model accepts, so honouring a stale override
|
|
2147
|
+
* would put an unsupported effort on every single request instead of letting
|
|
2148
|
+
* the harness reject it before provider I/O. The override then simply falls
|
|
2149
|
+
* back to the provider's own default until the user picks a level the catalog
|
|
2150
|
+
* still lists.
|
|
2151
|
+
*
|
|
2152
|
+
* `extendable` opts into the opposite rule for a base that is a *built-in
|
|
2153
|
+
* fallback* rather than discovered truth (codex, whose static effort list is
|
|
2154
|
+
* known to trail the backend): there, appending the configured level is how a
|
|
2155
|
+
* newly shipped tier becomes selectable at all.
|
|
2156
|
+
* @param configuredDefault - the user-configured default effort id, or undefined.
|
|
2157
|
+
* @param base - the discovered/built-in reasoning block, or undefined.
|
|
2158
|
+
* @param options - `extendable` marks the base as a fallback that may be extended.
|
|
2159
|
+
* @returns the merged block, or undefined when neither side contributes one.
|
|
2160
|
+
*/
|
|
2161
|
+
function mergeReasoning(configuredDefault, base, options) {
|
|
2162
|
+
const detached = base === void 0 ? void 0 : {
|
|
2163
|
+
efforts: [...base.efforts],
|
|
2164
|
+
...base.defaultEffort === void 0 ? {} : { defaultEffort: base.defaultEffort }
|
|
2165
|
+
};
|
|
2166
|
+
if (configuredDefault === void 0) return detached;
|
|
2167
|
+
const effort = ReasoningEffortId(configuredDefault);
|
|
2168
|
+
if (base === void 0) return options?.extendable === true ? {
|
|
2169
|
+
efforts: [{
|
|
2170
|
+
id: effort,
|
|
2171
|
+
name: effortDisplayName(effort)
|
|
2172
|
+
}],
|
|
2173
|
+
defaultEffort: effort
|
|
2174
|
+
} : void 0;
|
|
2175
|
+
if (base.efforts.some((entry) => entry.id === effort)) return {
|
|
2176
|
+
efforts: [...base.efforts],
|
|
2177
|
+
defaultEffort: effort
|
|
2178
|
+
};
|
|
2179
|
+
if (options?.extendable !== true) return detached;
|
|
2180
|
+
return {
|
|
2181
|
+
efforts: [...base.efforts, {
|
|
2182
|
+
id: effort,
|
|
2183
|
+
name: effortDisplayName(effort)
|
|
2184
|
+
}],
|
|
2185
|
+
defaultEffort: effort
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
1636
2188
|
/**
|
|
1637
2189
|
* First account catalog that lists `model` (callers pass default-first).
|
|
1638
2190
|
* One failing lookup sits that account out so a sibling's metadata still
|
|
@@ -2557,7 +3109,8 @@ var PoolUsageTracker = class {
|
|
|
2557
3109
|
/**
|
|
2558
3110
|
* The quota view of one member. A cold cache awaits the first fetch; a
|
|
2559
3111
|
* stale one answers immediately while the refresh serves the NEXT call
|
|
2560
|
-
* (member selection must never block on the network mid-conversation).
|
|
3112
|
+
* (member selection must never block on the network mid-conversation). A
|
|
3113
|
+
* failure still cooling down degrades immediately with no network call.
|
|
2561
3114
|
* @param member - the pool member to score (account resolved).
|
|
2562
3115
|
* @returns availability plus the urgency score.
|
|
2563
3116
|
*/
|
|
@@ -2570,10 +3123,13 @@ var PoolUsageTracker = class {
|
|
|
2570
3123
|
fetchedAt: 0
|
|
2571
3124
|
};
|
|
2572
3125
|
const entry = this.entries.get(key);
|
|
2573
|
-
if (entry !== void 0 && Date.now() - entry.at < this.ttlMs) return this.score(member, entry);
|
|
2574
3126
|
if (entry !== void 0) {
|
|
2575
|
-
|
|
2576
|
-
|
|
3127
|
+
const fresh = Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs);
|
|
3128
|
+
if (entry.snapshot !== void 0) {
|
|
3129
|
+
if (!fresh) this.refresh(key, fetcher).catch(() => void 0);
|
|
3130
|
+
return this.score(member, entry);
|
|
3131
|
+
}
|
|
3132
|
+
if (fresh) return degradedQuota(entry.error);
|
|
2577
3133
|
}
|
|
2578
3134
|
try {
|
|
2579
3135
|
const snapshot = await this.refresh(key, fetcher);
|
|
@@ -2582,17 +3138,31 @@ var PoolUsageTracker = class {
|
|
|
2582
3138
|
at: Date.now()
|
|
2583
3139
|
});
|
|
2584
3140
|
} catch (error) {
|
|
2585
|
-
return
|
|
2586
|
-
available: false,
|
|
2587
|
-
urgency: 0,
|
|
2588
|
-
fetchedAt: 0
|
|
2589
|
-
} : {
|
|
2590
|
-
available: true,
|
|
2591
|
-
urgency: 0,
|
|
2592
|
-
fetchedAt: 0
|
|
2593
|
-
};
|
|
3141
|
+
return degradedQuota(error);
|
|
2594
3142
|
}
|
|
2595
3143
|
}
|
|
3144
|
+
/**
|
|
3145
|
+
* Same cache as {@link quotaFor}, for direct display (the Settings page):
|
|
3146
|
+
* the raw snapshot, or the original fetch error, instead of a routing
|
|
3147
|
+
* score.
|
|
3148
|
+
* @param provider - the account's provider.
|
|
3149
|
+
* @param account - the account key.
|
|
3150
|
+
* @param force - bypass a fresh cached SNAPSHOT for an honest re-check (the
|
|
3151
|
+
* manual Refresh button). A live failure cooldown is never bypassed —
|
|
3152
|
+
* retrying through it is exactly what turns a 429 into a permanent
|
|
3153
|
+
* lockout, so even a forced call still answers from the negative cache.
|
|
3154
|
+
* @returns `{ supported: false }` when the provider has no usage fetcher.
|
|
3155
|
+
*/
|
|
3156
|
+
async snapshotFor(provider, account, force = false) {
|
|
3157
|
+
const fetcher = this.fetcherFor(provider, account);
|
|
3158
|
+
if (fetcher === void 0) return { supported: false };
|
|
3159
|
+
const key = `${provider}/${account}`;
|
|
3160
|
+
const entry = this.entries.get(key);
|
|
3161
|
+
if (entry !== void 0 && Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) if (entry.snapshot !== void 0) {
|
|
3162
|
+
if (!force) return entry.snapshot;
|
|
3163
|
+
} else throw entry.error;
|
|
3164
|
+
return this.refresh(key, fetcher);
|
|
3165
|
+
}
|
|
2596
3166
|
/** Drop cached snapshots: one account, or a whole provider when `account` is omitted. */
|
|
2597
3167
|
invalidate(provider, account) {
|
|
2598
3168
|
if (account !== void 0) {
|
|
@@ -2601,7 +3171,14 @@ var PoolUsageTracker = class {
|
|
|
2601
3171
|
}
|
|
2602
3172
|
for (const key of [...this.entries.keys()]) if (key.startsWith(`${provider}/`)) this.entries.delete(key);
|
|
2603
3173
|
}
|
|
2604
|
-
/**
|
|
3174
|
+
/**
|
|
3175
|
+
* Run (or join) the single in-flight fetch for one account key, caching
|
|
3176
|
+
* either outcome. A missing/invalid credential is deliberately NOT
|
|
3177
|
+
* negative-cached: it costs no network round trip (the session lookup
|
|
3178
|
+
* fails before the request goes out) and re-checking live means the
|
|
3179
|
+
* member rejoins routing the instant its login is fixed, rather than
|
|
3180
|
+
* waiting out a stale cooldown.
|
|
3181
|
+
*/
|
|
2605
3182
|
refresh(key, fetcher) {
|
|
2606
3183
|
let pending = this.inflight.get(key);
|
|
2607
3184
|
if (pending === void 0) {
|
|
@@ -2611,6 +3188,13 @@ var PoolUsageTracker = class {
|
|
|
2611
3188
|
at: Date.now()
|
|
2612
3189
|
});
|
|
2613
3190
|
return snapshot;
|
|
3191
|
+
}, (error) => {
|
|
3192
|
+
if (!isMissingOrInvalidCredential(error)) this.entries.set(key, {
|
|
3193
|
+
error,
|
|
3194
|
+
at: Date.now(),
|
|
3195
|
+
cooldownMs: cooldownFor(error, this.ttlMs)
|
|
3196
|
+
});
|
|
3197
|
+
throw error;
|
|
2614
3198
|
}).finally(() => {
|
|
2615
3199
|
this.inflight.delete(key);
|
|
2616
3200
|
});
|
|
@@ -2635,6 +3219,27 @@ var PoolUsageTracker = class {
|
|
|
2635
3219
|
}
|
|
2636
3220
|
};
|
|
2637
3221
|
/**
|
|
3222
|
+
* The routing view of a fetch failure. Logged out: the member cannot serve
|
|
3223
|
+
* at all. Any other failure (network, endpoint rate limit) must not block
|
|
3224
|
+
* routing — the member stays available with a zero score, degrading the
|
|
3225
|
+
* strategy to plain priority order for it.
|
|
3226
|
+
*/
|
|
3227
|
+
function degradedQuota(error) {
|
|
3228
|
+
return isMissingOrInvalidCredential(error) ? {
|
|
3229
|
+
available: false,
|
|
3230
|
+
urgency: 0,
|
|
3231
|
+
fetchedAt: 0
|
|
3232
|
+
} : {
|
|
3233
|
+
available: true,
|
|
3234
|
+
urgency: 0,
|
|
3235
|
+
fetchedAt: 0
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
/** How long to hold a failure in the negative cache: the endpoint's own `retry-after`, or the default TTL. */
|
|
3239
|
+
function cooldownFor(error, defaultTtlMs) {
|
|
3240
|
+
return error instanceof OAuthEndpointError && error.retryAfterMs !== void 0 ? error.retryAfterMs : defaultTtlMs;
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
2638
3243
|
* Whether a window constrains this model: unscoped windows always do; a
|
|
2639
3244
|
* model-scoped window (Claude's Opus/Sonnet lanes) applies when its scope
|
|
2640
3245
|
* names the model family.
|
|
@@ -2701,6 +3306,14 @@ async function resolveImages(messages, attachments, signal) {
|
|
|
2701
3306
|
})));
|
|
2702
3307
|
}
|
|
2703
3308
|
|
|
3309
|
+
//#endregion
|
|
3310
|
+
//#region src/compat.ts
|
|
3311
|
+
/** Brand a string as a tool-call id: alpha's `ToolCallId`, rc.2's `CallId`. */
|
|
3312
|
+
const ToolCallId = (() => {
|
|
3313
|
+
const exports = llm;
|
|
3314
|
+
return exports["ToolCallId"] ?? exports["CallId"];
|
|
3315
|
+
})();
|
|
3316
|
+
|
|
2704
3317
|
//#endregion
|
|
2705
3318
|
//#region src/translate/sse.ts
|
|
2706
3319
|
/**
|
|
@@ -2895,7 +3508,7 @@ function closeBlock$2(block) {
|
|
|
2895
3508
|
};
|
|
2896
3509
|
case "tool-call": return {
|
|
2897
3510
|
type: "tool-call",
|
|
2898
|
-
id:
|
|
3511
|
+
id: ToolCallId(block.callId),
|
|
2899
3512
|
name: block.name ?? "",
|
|
2900
3513
|
arguments: block.text
|
|
2901
3514
|
};
|
|
@@ -2985,7 +3598,7 @@ var ResponsesStreamTranslator = class {
|
|
|
2985
3598
|
chunks.push({
|
|
2986
3599
|
type: "tool-call-delta",
|
|
2987
3600
|
index: block.index,
|
|
2988
|
-
id:
|
|
3601
|
+
id: ToolCallId(callId),
|
|
2989
3602
|
...item.name === void 0 ? {} : { name: item.name },
|
|
2990
3603
|
argumentsDelta: ""
|
|
2991
3604
|
});
|
|
@@ -3027,7 +3640,7 @@ var ResponsesStreamTranslator = class {
|
|
|
3027
3640
|
chunks.push({
|
|
3028
3641
|
type: "tool-call-delta",
|
|
3029
3642
|
index: block.index,
|
|
3030
|
-
id:
|
|
3643
|
+
id: ToolCallId(block.callId),
|
|
3031
3644
|
...block.name === void 0 ? {} : { name: block.name },
|
|
3032
3645
|
argumentsDelta: event.delta ?? ""
|
|
3033
3646
|
});
|
|
@@ -3120,6 +3733,28 @@ const CODEX_CONTEXT_WINDOW = 4e5;
|
|
|
3120
3733
|
const CODEX_DEFAULT_MAX_TOKENS = 128e3;
|
|
3121
3734
|
/** Refresh when the access token has less than this much life left. */
|
|
3122
3735
|
const CODEX_PREEMPT_MS = 5 * 6e4;
|
|
3736
|
+
/**
|
|
3737
|
+
* Body fields the backend uses to name a reset. A window-exhaustion rejection
|
|
3738
|
+
* carries `usage_limit_reached` with the seconds left on the window — the case
|
|
3739
|
+
* that used to classify as a terminal quota and never be retried at all.
|
|
3740
|
+
*/
|
|
3741
|
+
const CODEX_RESET_FIELDS = [
|
|
3742
|
+
"resets_in_seconds",
|
|
3743
|
+
"reset_after_seconds",
|
|
3744
|
+
"resets_at",
|
|
3745
|
+
"reset_at"
|
|
3746
|
+
];
|
|
3747
|
+
/**
|
|
3748
|
+
* Reads the reset instant of the Codex window that rejected a request.
|
|
3749
|
+
*
|
|
3750
|
+
* Body only. The `x-codex-{primary,secondary}-reset-after-seconds` headers are
|
|
3751
|
+
* rollover snapshots the backend attaches to every response, one per window,
|
|
3752
|
+
* so they say nothing about which window refused: a burst 429 that would clear
|
|
3753
|
+
* in seconds still carries a primary rollover hours out, and reading it would
|
|
3754
|
+
* park the turn for those hours. They reach the operator through
|
|
3755
|
+
* `rateLimitDiagnostics` instead.
|
|
3756
|
+
*/
|
|
3757
|
+
const codexRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), CODEX_RESET_FIELDS, now);
|
|
3123
3758
|
/** Default instruction when the request carries no system prompt. */
|
|
3124
3759
|
const DEFAULT_CODEX_INSTRUCTIONS = "You are Codex, a coding agent based on GPT-5. Help the user with their software engineering tasks.";
|
|
3125
3760
|
/** Refresh-grant rejections that mean the login is gone for good. */
|
|
@@ -3368,10 +4003,6 @@ const CODEX_MODELS_URL = "https://chatgpt.com/backend-api/codex/models";
|
|
|
3368
4003
|
* the range of current codex CLI releases.
|
|
3369
4004
|
*/
|
|
3370
4005
|
const CODEX_CLIENT_VERSION = "0.147.0";
|
|
3371
|
-
/** Display name for a wire reasoning-effort value. */
|
|
3372
|
-
function effortName(effort) {
|
|
3373
|
-
return effort === "xhigh" ? "Extra High" : effort.charAt(0).toUpperCase() + effort.slice(1);
|
|
3374
|
-
}
|
|
3375
4006
|
/**
|
|
3376
4007
|
* Whether a catalog entry advertises the fast tier. Mirrors codex-rs
|
|
3377
4008
|
* `ModelPreset::supports_fast_mode`: a `service_tiers` id matching the fast
|
|
@@ -3407,7 +4038,7 @@ async function fetchCodexModels(session, fetchFn = proxiedFetch, signal) {
|
|
|
3407
4038
|
if (entry.visibility === "hide" || entry.visibility === "none") continue;
|
|
3408
4039
|
const efforts = (entry.supported_reasoning_levels ?? []).filter((level) => typeof level.effort === "string" && level.effort.length > 0).map((level) => ({
|
|
3409
4040
|
id: ReasoningEffortId(level.effort),
|
|
3410
|
-
name:
|
|
4041
|
+
name: effortDisplayName(level.effort),
|
|
3411
4042
|
...level.description === void 0 ? {} : { description: level.description }
|
|
3412
4043
|
}));
|
|
3413
4044
|
const defaultEffort = typeof entry.default_reasoning_level === "string" && entry.default_reasoning_level.length > 0 && efforts.some((effort) => effort.id === ReasoningEffortId(entry.default_reasoning_level)) ? ReasoningEffortId(entry.default_reasoning_level) : void 0;
|
|
@@ -3545,6 +4176,9 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
3545
4176
|
name: "ChatGPT (Codex)"
|
|
3546
4177
|
};
|
|
3547
4178
|
}
|
|
4179
|
+
providerRetryPolicy(provider) {
|
|
4180
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `codex: provider "${provider}" retryPolicy`);
|
|
4181
|
+
}
|
|
3548
4182
|
staticModels(provider) {
|
|
3549
4183
|
return this.options.models.map((model) => ({
|
|
3550
4184
|
provider,
|
|
@@ -3633,6 +4267,10 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
3633
4267
|
async resolveOwnModel(provider, model) {
|
|
3634
4268
|
const discovered = await this.discovered(model);
|
|
3635
4269
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
4270
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning ?? {
|
|
4271
|
+
efforts: CODEX_EFFORTS,
|
|
4272
|
+
defaultEffort: CODEX_DEFAULT_EFFORT
|
|
4273
|
+
}, { extendable: discovered?.reasoning === void 0 });
|
|
3636
4274
|
return {
|
|
3637
4275
|
provider,
|
|
3638
4276
|
id: model,
|
|
@@ -3641,10 +4279,7 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
3641
4279
|
inputModalities: configured?.inputModalities ?? CODEX_MODALITIES,
|
|
3642
4280
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? CODEX_CONTEXT_WINDOW },
|
|
3643
4281
|
defaultMaxTokens: configured?.maxTokens ?? CODEX_DEFAULT_MAX_TOKENS,
|
|
3644
|
-
reasoning:
|
|
3645
|
-
efforts: CODEX_EFFORTS,
|
|
3646
|
-
defaultEffort: CODEX_DEFAULT_EFFORT
|
|
3647
|
-
}
|
|
4282
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
3648
4283
|
};
|
|
3649
4284
|
}
|
|
3650
4285
|
async *stream(options) {
|
|
@@ -3668,7 +4303,10 @@ var CodexAdapter = class extends LlmAdapter {
|
|
|
3668
4303
|
session = await this.options.tokens.session(account, true);
|
|
3669
4304
|
response = await this.request(options, session, watchdog.signal);
|
|
3670
4305
|
}
|
|
3671
|
-
if (!response.ok) throw await httpLlmError(response, "codex API"
|
|
4306
|
+
if (!response.ok) throw await httpLlmError(response, "codex API", {
|
|
4307
|
+
rateLimitReset: codexRateLimitReset,
|
|
4308
|
+
...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
|
|
4309
|
+
});
|
|
3672
4310
|
if (response.body === null) throw new LlmError("codex API returned no response body", EMPTY_RESPONSE_CODE);
|
|
3673
4311
|
yield* streamResponses(response.body, () => {
|
|
3674
4312
|
watchdog.pulse();
|
|
@@ -3921,7 +4559,7 @@ function closeBlock$1(block) {
|
|
|
3921
4559
|
};
|
|
3922
4560
|
case "tool-call": return {
|
|
3923
4561
|
type: "tool-call",
|
|
3924
|
-
id:
|
|
4562
|
+
id: ToolCallId(block.callId),
|
|
3925
4563
|
name: block.name ?? "",
|
|
3926
4564
|
arguments: block.text
|
|
3927
4565
|
};
|
|
@@ -4024,7 +4662,7 @@ var AnthropicStreamTranslator = class {
|
|
|
4024
4662
|
chunks.push({
|
|
4025
4663
|
type: "tool-call-delta",
|
|
4026
4664
|
index: opened.index,
|
|
4027
|
-
id:
|
|
4665
|
+
id: ToolCallId(opened.callId),
|
|
4028
4666
|
...block.name === void 0 ? {} : { name: block.name },
|
|
4029
4667
|
argumentsDelta: ""
|
|
4030
4668
|
});
|
|
@@ -4061,7 +4699,7 @@ var AnthropicStreamTranslator = class {
|
|
|
4061
4699
|
chunks.push({
|
|
4062
4700
|
type: "tool-call-delta",
|
|
4063
4701
|
index: block.index,
|
|
4064
|
-
id:
|
|
4702
|
+
id: ToolCallId(block.callId),
|
|
4065
4703
|
...block.name === void 0 ? {} : { name: block.name },
|
|
4066
4704
|
argumentsDelta: delta.partial_json ?? ""
|
|
4067
4705
|
});
|
|
@@ -4165,6 +4803,34 @@ const CLAUDE_DEFAULT_MAX_TOKENS = 32e3;
|
|
|
4165
4803
|
/** Refresh when the access token has less than this much life left. */
|
|
4166
4804
|
const CLAUDE_PREEMPT_MS = 5 * 6e4;
|
|
4167
4805
|
/**
|
|
4806
|
+
* Body fields Anthropic uses to name a reset instant, read when the unified
|
|
4807
|
+
* headers are absent.
|
|
4808
|
+
*/
|
|
4809
|
+
const CLAUDE_RESET_FIELDS = [
|
|
4810
|
+
"resets_at",
|
|
4811
|
+
"resetsAt",
|
|
4812
|
+
"reset_at",
|
|
4813
|
+
"retry_after"
|
|
4814
|
+
];
|
|
4815
|
+
/**
|
|
4816
|
+
* Reads the reset instant of the Anthropic window that rejected a request.
|
|
4817
|
+
*
|
|
4818
|
+
* `anthropic-ratelimit-unified-*` is the subscription-plan family — the one
|
|
4819
|
+
* Claude Code renders as "resets 3pm" — and is the only header that names the
|
|
4820
|
+
* window which actually rejected this request. The per-bucket
|
|
4821
|
+
* `anthropic-ratelimit-{requests,tokens,input-tokens,output-tokens}-reset`
|
|
4822
|
+
* headers are deliberately not read: they are rollover snapshots attached to
|
|
4823
|
+
* every response, so on a 429 they cannot say which bucket refused, and the
|
|
4824
|
+
* earliest of them is typically the bucket that still had room — a wait that
|
|
4825
|
+
* lands straight back in the closed window. They reach the operator through
|
|
4826
|
+
* `rateLimitDiagnostics` instead.
|
|
4827
|
+
*/
|
|
4828
|
+
const claudeRateLimitReset = (response, body, now) => {
|
|
4829
|
+
const unified = earliestReset(resetInstantFromHeader(response, "anthropic-ratelimit-unified-reset", now), resetInstantFromHeader(response, "anthropic-ratelimit-unified-fallback-reset", now));
|
|
4830
|
+
if (unified !== void 0) return unified;
|
|
4831
|
+
return resetFromFields(jsonBody(body), CLAUDE_RESET_FIELDS, now);
|
|
4832
|
+
};
|
|
4833
|
+
/**
|
|
4168
4834
|
* The subscription endpoint only serves requests presenting as Claude Code,
|
|
4169
4835
|
* so these headers impersonate the CLI; the harness attribution user-agent
|
|
4170
4836
|
* cannot be sent here (one user-agent slot, and the CLI's wins).
|
|
@@ -4432,14 +5098,6 @@ async function fetchClaudeModels(session, fetchFn = proxiedFetch, signal) {
|
|
|
4432
5098
|
if (models.length === 0) throw new Error("claude models API returned an empty catalog");
|
|
4433
5099
|
return models;
|
|
4434
5100
|
}
|
|
4435
|
-
/**
|
|
4436
|
-
* Claude Code's own SDK retry shape: exponential backoff starting at 1s,
|
|
4437
|
-
* doubling per attempt, capped at 60s, plus jitter. `maxRetries` is the
|
|
4438
|
-
* count of retries after the first attempt (Claude Code defaults to 10).
|
|
4439
|
-
*/
|
|
4440
|
-
const CLAUDE_RETRY_INITIAL_DELAY_MS = 1e3;
|
|
4441
|
-
const CLAUDE_RETRY_MAX_DELAY_MS = 6e4;
|
|
4442
|
-
const CLAUDE_RETRY_JITTER_RATIO = .2;
|
|
4443
5101
|
/** The Claude 4.5 family accepts image input. */
|
|
4444
5102
|
const CLAUDE_MODALITIES = ["text", "image"];
|
|
4445
5103
|
/**
|
|
@@ -4533,16 +5191,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
4533
5191
|
};
|
|
4534
5192
|
}
|
|
4535
5193
|
providerRetryPolicy(provider) {
|
|
4536
|
-
|
|
4537
|
-
return resolveRetryPolicy({
|
|
4538
|
-
mode: "normal",
|
|
4539
|
-
maxRetries: this.options.maxRetries,
|
|
4540
|
-
backoff: {
|
|
4541
|
-
initialDelayMs: CLAUDE_RETRY_INITIAL_DELAY_MS,
|
|
4542
|
-
maxDelayMs: CLAUDE_RETRY_MAX_DELAY_MS,
|
|
4543
|
-
jitterRatio: CLAUDE_RETRY_JITTER_RATIO
|
|
4544
|
-
}
|
|
4545
|
-
}, `claude: provider "${provider}" retryPolicy`);
|
|
5194
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `claude: provider "${provider}" retryPolicy`);
|
|
4546
5195
|
}
|
|
4547
5196
|
async listModels(provider) {
|
|
4548
5197
|
const own = await this.listOwnModels(provider);
|
|
@@ -4588,7 +5237,7 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
4588
5237
|
async resolveOwnModel(provider, model) {
|
|
4589
5238
|
const disc = await this.discovered(model);
|
|
4590
5239
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
4591
|
-
const reasoning = disc?.reasoning;
|
|
5240
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), disc?.reasoning);
|
|
4592
5241
|
return {
|
|
4593
5242
|
provider,
|
|
4594
5243
|
id: model,
|
|
@@ -4620,7 +5269,10 @@ var ClaudeAdapter = class extends LlmAdapter {
|
|
|
4620
5269
|
session = await this.options.tokens.session(account, true);
|
|
4621
5270
|
response = await this.request(options, session, watchdog.signal);
|
|
4622
5271
|
}
|
|
4623
|
-
if (!response.ok) throw await httpLlmError(response, "claude API"
|
|
5272
|
+
if (!response.ok) throw await httpLlmError(response, "claude API", {
|
|
5273
|
+
rateLimitReset: claudeRateLimitReset,
|
|
5274
|
+
...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
|
|
5275
|
+
});
|
|
4624
5276
|
if (response.body === null) throw new LlmError("claude API returned no response body", EMPTY_RESPONSE_CODE);
|
|
4625
5277
|
yield* streamAnthropic(response.body, () => {
|
|
4626
5278
|
watchdog.pulse();
|
|
@@ -4687,6 +5339,24 @@ const GROK_CONTEXT_WINDOW = 256e3;
|
|
|
4687
5339
|
const GROK_DEFAULT_MAX_TOKENS = 32e3;
|
|
4688
5340
|
/** Refresh when the access token has less than this much life left. */
|
|
4689
5341
|
const GROK_PREEMPT_MS = 2 * 6e4;
|
|
5342
|
+
/** Body fields xAI uses to name a delay or reset. */
|
|
5343
|
+
const GROK_RESET_FIELDS = [
|
|
5344
|
+
"retry_after",
|
|
5345
|
+
"retry_after_seconds",
|
|
5346
|
+
"resets_at",
|
|
5347
|
+
"reset_at"
|
|
5348
|
+
];
|
|
5349
|
+
/**
|
|
5350
|
+
* Reads the reset instant of the xAI window that rejected a request.
|
|
5351
|
+
*
|
|
5352
|
+
* Body only. xAI serves the OpenAI-compatible `x-ratelimit-reset-*` family,
|
|
5353
|
+
* whose values are rollover durations (`6m0s`) present on every response, one
|
|
5354
|
+
* per bucket — on a 429 the earliest of them is usually a bucket with room
|
|
5355
|
+
* (`0s` for the request bucket while the token bucket is the one exhausted),
|
|
5356
|
+
* which would burn the whole retry budget in seconds. They reach the operator
|
|
5357
|
+
* through `rateLimitDiagnostics` instead.
|
|
5358
|
+
*/
|
|
5359
|
+
const grokRateLimitReset = (_response, body, now) => resetFromFields(jsonBody(body), GROK_RESET_FIELDS, now);
|
|
4690
5360
|
/** A discovered URL must be https on x.ai or a subdomain; anything else is a hostile document. */
|
|
4691
5361
|
function assertXaiEndpoint(url, field) {
|
|
4692
5362
|
let parsed;
|
|
@@ -5106,6 +5776,9 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
5106
5776
|
name: "Grok (Subscription)"
|
|
5107
5777
|
};
|
|
5108
5778
|
}
|
|
5779
|
+
providerRetryPolicy(provider) {
|
|
5780
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `grok: provider "${provider}" retryPolicy`);
|
|
5781
|
+
}
|
|
5109
5782
|
staticModels(provider) {
|
|
5110
5783
|
return this.options.models.map((model) => ({
|
|
5111
5784
|
provider,
|
|
@@ -5167,6 +5840,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
5167
5840
|
async resolveOwnModel(provider, model) {
|
|
5168
5841
|
const discovered = await this.discovered(model);
|
|
5169
5842
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
5843
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
|
|
5170
5844
|
return {
|
|
5171
5845
|
provider,
|
|
5172
5846
|
id: model,
|
|
@@ -5175,7 +5849,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
5175
5849
|
inputModalities: configured?.inputModalities ?? grokModalities(model),
|
|
5176
5850
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? GROK_CONTEXT_WINDOW },
|
|
5177
5851
|
defaultMaxTokens: configured?.maxTokens ?? GROK_DEFAULT_MAX_TOKENS,
|
|
5178
|
-
...
|
|
5852
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
5179
5853
|
};
|
|
5180
5854
|
}
|
|
5181
5855
|
async *stream(options) {
|
|
@@ -5199,7 +5873,10 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
5199
5873
|
session = await this.options.tokens.session(account, true);
|
|
5200
5874
|
response = await this.request(options, session, watchdog.signal);
|
|
5201
5875
|
}
|
|
5202
|
-
if (!response.ok) throw await httpLlmError(response, "grok API"
|
|
5876
|
+
if (!response.ok) throw await httpLlmError(response, "grok API", {
|
|
5877
|
+
rateLimitReset: grokRateLimitReset,
|
|
5878
|
+
...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn }
|
|
5879
|
+
});
|
|
5203
5880
|
if (response.body === null) throw new LlmError("grok API returned no response body", EMPTY_RESPONSE_CODE);
|
|
5204
5881
|
yield* streamResponses(response.body, () => {
|
|
5205
5882
|
watchdog.pulse();
|
|
@@ -5221,6 +5898,7 @@ var GrokAdapter = class extends LlmAdapter {
|
|
|
5221
5898
|
parallel_tool_calls: true,
|
|
5222
5899
|
...options.maxTokens !== void 0 ? { max_output_tokens: options.maxTokens } : {},
|
|
5223
5900
|
...options.reasoningEffort !== void 0 ? { reasoning: { effort: String(options.reasoningEffort) } } : {},
|
|
5901
|
+
...options.sessionId !== void 0 ? { prompt_cache_key: String(options.sessionId) } : {},
|
|
5224
5902
|
store: false,
|
|
5225
5903
|
stream: true
|
|
5226
5904
|
};
|
|
@@ -5383,7 +6061,7 @@ function closeBlock(block) {
|
|
|
5383
6061
|
};
|
|
5384
6062
|
case "tool-call": return {
|
|
5385
6063
|
type: "tool-call",
|
|
5386
|
-
id:
|
|
6064
|
+
id: ToolCallId(block.callId),
|
|
5387
6065
|
name: block.name ?? "",
|
|
5388
6066
|
arguments: block.text
|
|
5389
6067
|
};
|
|
@@ -5537,7 +6215,7 @@ var ChatCompletionsStreamTranslator = class {
|
|
|
5537
6215
|
chunks.push({
|
|
5538
6216
|
type: "tool-call-delta",
|
|
5539
6217
|
index: block.index,
|
|
5540
|
-
id:
|
|
6218
|
+
id: ToolCallId(block.callId),
|
|
5541
6219
|
...block.name === void 0 ? {} : { name: block.name },
|
|
5542
6220
|
argumentsDelta: ""
|
|
5543
6221
|
});
|
|
@@ -5547,7 +6225,7 @@ var ChatCompletionsStreamTranslator = class {
|
|
|
5547
6225
|
chunks.push({
|
|
5548
6226
|
type: "tool-call-delta",
|
|
5549
6227
|
index: block.index,
|
|
5550
|
-
id:
|
|
6228
|
+
id: ToolCallId(block.callId),
|
|
5551
6229
|
argumentsDelta: call.function.arguments
|
|
5552
6230
|
});
|
|
5553
6231
|
}
|
|
@@ -6096,6 +6774,9 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
6096
6774
|
name: "GitHub Copilot"
|
|
6097
6775
|
};
|
|
6098
6776
|
}
|
|
6777
|
+
providerRetryPolicy(provider) {
|
|
6778
|
+
return subscriptionRetryPolicy(DEFAULT_RETRY, this.options.rateLimit ?? DEFAULT_RATE_LIMIT_WAIT, `copilot: provider "${provider}" retryPolicy`);
|
|
6779
|
+
}
|
|
6099
6780
|
staticModels(provider) {
|
|
6100
6781
|
return this.options.models.map((model) => ({
|
|
6101
6782
|
provider,
|
|
@@ -6258,6 +6939,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
6258
6939
|
async resolveOwnModel(provider, model) {
|
|
6259
6940
|
const discovered = await this.discovered(model);
|
|
6260
6941
|
const configured = this.options.models.find((entry) => entry.id === model);
|
|
6942
|
+
const reasoning = mergeReasoning(this.options.defaultEffortOf?.(model), discovered?.reasoning);
|
|
6261
6943
|
return {
|
|
6262
6944
|
provider,
|
|
6263
6945
|
id: model,
|
|
@@ -6266,7 +6948,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
6266
6948
|
inputModalities: discovered?.inputModalities ?? configured?.inputModalities ?? ["text"],
|
|
6267
6949
|
context: { contextWindow: discovered?.contextWindow ?? configured?.contextWindow ?? COPILOT_CONTEXT_WINDOW },
|
|
6268
6950
|
defaultMaxTokens: configured?.maxTokens ?? COPILOT_DEFAULT_MAX_TOKENS,
|
|
6269
|
-
...
|
|
6951
|
+
...reasoning === void 0 ? {} : { reasoning }
|
|
6270
6952
|
};
|
|
6271
6953
|
}
|
|
6272
6954
|
async *stream(options) {
|
|
@@ -6293,7 +6975,7 @@ var CopilotAdapter = class CopilotAdapter extends LlmAdapter {
|
|
|
6293
6975
|
session = await this.options.tokens.session(account, true);
|
|
6294
6976
|
response = await this.request(options, session, watchdog.signal, wire, scope);
|
|
6295
6977
|
}
|
|
6296
|
-
if (!response.ok) throw await httpLlmError(response, "copilot API");
|
|
6978
|
+
if (!response.ok) throw await httpLlmError(response, "copilot API", { ...this.options.onWarn === void 0 ? {} : { onWarn: this.options.onWarn } });
|
|
6297
6979
|
if (response.body === null) throw new LlmError("copilot API returned no response body", EMPTY_RESPONSE_CODE);
|
|
6298
6980
|
const pulse = () => {
|
|
6299
6981
|
watchdog.pulse();
|
|
@@ -6616,13 +7298,13 @@ function truncate$1(text, max = 60) {
|
|
|
6616
7298
|
* image input; any resolution failure means "no".
|
|
6617
7299
|
*/
|
|
6618
7300
|
async function routeDeclaresImageInput(resolveLlm, exec) {
|
|
6619
|
-
const llm = resolveLlm?.();
|
|
7301
|
+
const llm$1 = resolveLlm?.();
|
|
6620
7302
|
const routed = exec.agent?.session.requestHeader()?.config;
|
|
6621
7303
|
const provider = routed?.provider ?? exec.agent?.options.provider;
|
|
6622
7304
|
const model = routed?.model ?? exec.agent?.options.model;
|
|
6623
|
-
if (llm === void 0 || provider === void 0 || model === void 0) return false;
|
|
7305
|
+
if (llm$1 === void 0 || provider === void 0 || model === void 0) return false;
|
|
6624
7306
|
try {
|
|
6625
|
-
return (await llm.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes("image") === true;
|
|
7307
|
+
return (await llm$1.resolveModelInfo(provider, model, exec.signal)).inputModalities?.includes("image") === true;
|
|
6626
7308
|
} catch {
|
|
6627
7309
|
return false;
|
|
6628
7310
|
}
|
|
@@ -7111,6 +7793,10 @@ const Config = z.object({
|
|
|
7111
7793
|
"copilot"
|
|
7112
7794
|
]),
|
|
7113
7795
|
streamIdleTimeoutMs: z.number().min(1).default(DEFAULT_STREAM_IDLE_TIMEOUT_MS),
|
|
7796
|
+
rateLimit: z.object({
|
|
7797
|
+
wait: z.boolean().default(true),
|
|
7798
|
+
maxWaitMs: z.number().min(1).default(DEFAULT_RATE_LIMIT_MAX_WAIT_MS)
|
|
7799
|
+
}),
|
|
7114
7800
|
models: z.object({
|
|
7115
7801
|
codex: z.array(modelEntrySchema),
|
|
7116
7802
|
claude: z.array(modelEntrySchema),
|
|
@@ -7273,18 +7959,20 @@ var SubscriptionsAuthController = class {
|
|
|
7273
7959
|
* cannot be read off the flow manager.
|
|
7274
7960
|
*/
|
|
7275
7961
|
claims = /* @__PURE__ */ new Map();
|
|
7276
|
-
constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials) {
|
|
7962
|
+
constructor(flows, deviceFlows, onAuthChanged, resolveAttachments, usageFetchers = {}, readClaudeCreds = readClaudeCodeCredentials, poolUsage = void 0) {
|
|
7277
7963
|
this.flows = flows;
|
|
7278
7964
|
this.deviceFlows = deviceFlows;
|
|
7279
7965
|
this.onAuthChanged = onAuthChanged;
|
|
7280
7966
|
this.resolveAttachments = resolveAttachments;
|
|
7281
7967
|
this.usageFetchers = usageFetchers;
|
|
7282
7968
|
this.readClaudeCreds = readClaudeCreds;
|
|
7969
|
+
this.poolUsage = poolUsage;
|
|
7283
7970
|
}
|
|
7284
|
-
usage(provider, account, signal) {
|
|
7971
|
+
usage(provider, account, signal, force = false) {
|
|
7285
7972
|
const fetcher = this.usageFetchers[provider];
|
|
7286
7973
|
if (fetcher === void 0) return Promise.resolve({ supported: false });
|
|
7287
|
-
return fetcher(account, signal);
|
|
7974
|
+
if (this.poolUsage === void 0) return fetcher(account, signal);
|
|
7975
|
+
return this.poolUsage.snapshotFor(provider, account, force);
|
|
7288
7976
|
}
|
|
7289
7977
|
async readImage(ref, signal) {
|
|
7290
7978
|
const attachments = this.resolveAttachments();
|
|
@@ -7451,6 +8139,7 @@ function apply(ctx, config) {
|
|
|
7451
8139
|
const providers = [...new Set(config.providers ?? [...PROVIDER_IDS])];
|
|
7452
8140
|
const streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? DEFAULT_STREAM_IDLE_TIMEOUT_MS;
|
|
7453
8141
|
if (!Number.isFinite(streamIdleTimeoutMs) || streamIdleTimeoutMs <= 0) throw new Error(`${name}: streamIdleTimeoutMs must be a positive finite number`);
|
|
8142
|
+
const rateLimit = resolveRateLimitWait(config.rateLimit, `${name}: rateLimit`);
|
|
7454
8143
|
const catalog = resolveCatalog(config.models);
|
|
7455
8144
|
const overridden = new Set(PROVIDER_IDS.filter((provider) => (config.models?.[provider]?.length ?? 0) > 0));
|
|
7456
8145
|
const flows = new OAuthFlowManager();
|
|
@@ -7473,6 +8162,7 @@ function apply(ctx, config) {
|
|
|
7473
8162
|
poolAdapter?.invalidate();
|
|
7474
8163
|
for (const [route, handle] of handles) handle.replace([route]);
|
|
7475
8164
|
};
|
|
8165
|
+
loadModelDefaults();
|
|
7476
8166
|
let codexTokens;
|
|
7477
8167
|
let claudeTokens;
|
|
7478
8168
|
let grokTokens;
|
|
@@ -7501,11 +8191,13 @@ function apply(ctx, config) {
|
|
|
7501
8191
|
adapter = new CodexAdapter({
|
|
7502
8192
|
models: catalog.codex,
|
|
7503
8193
|
streamIdleTimeoutMs,
|
|
8194
|
+
rateLimit,
|
|
7504
8195
|
tokens,
|
|
7505
8196
|
discovery: !overridden.has("codex"),
|
|
7506
8197
|
onWarn,
|
|
7507
8198
|
resolveAttachments,
|
|
7508
8199
|
catalogStore: catalogStore("codex"),
|
|
8200
|
+
defaultEffortOf: (model) => defaultEffortOf("codex", model),
|
|
7509
8201
|
pool: () => poolAdapter,
|
|
7510
8202
|
speedFor: (sessionId, model) => sessionId !== void 0 && speedBySession.get(sessionId) === "fast" && adapter.supportsFastTier(model)
|
|
7511
8203
|
});
|
|
@@ -7533,12 +8225,13 @@ function apply(ctx, config) {
|
|
|
7533
8225
|
const adapter = new ClaudeAdapter({
|
|
7534
8226
|
models: catalog.claude,
|
|
7535
8227
|
streamIdleTimeoutMs,
|
|
8228
|
+
rateLimit,
|
|
7536
8229
|
tokens,
|
|
7537
8230
|
discovery: !overridden.has("claude"),
|
|
7538
8231
|
onWarn,
|
|
7539
|
-
maxRetries: 10,
|
|
7540
8232
|
resolveAttachments,
|
|
7541
8233
|
catalogStore: catalogStore("claude"),
|
|
8234
|
+
defaultEffortOf: (model) => defaultEffortOf("claude", model),
|
|
7542
8235
|
pool: () => poolAdapter
|
|
7543
8236
|
});
|
|
7544
8237
|
adapters.set("claude", adapter);
|
|
@@ -7564,11 +8257,13 @@ function apply(ctx, config) {
|
|
|
7564
8257
|
const adapter = new GrokAdapter({
|
|
7565
8258
|
models: catalog.grok,
|
|
7566
8259
|
streamIdleTimeoutMs,
|
|
8260
|
+
rateLimit,
|
|
7567
8261
|
tokens,
|
|
7568
8262
|
discovery: !overridden.has("grok"),
|
|
7569
8263
|
onWarn,
|
|
7570
8264
|
resolveAttachments,
|
|
7571
8265
|
catalogStore: catalogStore("grok"),
|
|
8266
|
+
defaultEffortOf: (model) => defaultEffortOf("grok", model),
|
|
7572
8267
|
pool: () => poolAdapter
|
|
7573
8268
|
});
|
|
7574
8269
|
adapters.set("grok", adapter);
|
|
@@ -7592,11 +8287,13 @@ function apply(ctx, config) {
|
|
|
7592
8287
|
copilotAdapter = new CopilotAdapter({
|
|
7593
8288
|
models: catalog.copilot,
|
|
7594
8289
|
streamIdleTimeoutMs,
|
|
8290
|
+
rateLimit,
|
|
7595
8291
|
tokens,
|
|
7596
8292
|
discovery: !overridden.has("copilot"),
|
|
7597
8293
|
onWarn,
|
|
7598
8294
|
resolveAttachments,
|
|
7599
8295
|
catalogStore: catalogStore("copilot"),
|
|
8296
|
+
defaultEffortOf: (model) => defaultEffortOf("copilot", model),
|
|
7600
8297
|
pool: () => poolAdapter
|
|
7601
8298
|
});
|
|
7602
8299
|
adapters.set("copilot", copilotAdapter);
|
|
@@ -7667,7 +8364,7 @@ function apply(ctx, config) {
|
|
|
7667
8364
|
onWarn
|
|
7668
8365
|
});
|
|
7669
8366
|
}
|
|
7670
|
-
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers), {
|
|
8367
|
+
registerAuthRpc(ctx, new SubscriptionsAuthController(flows, deviceFlows, authChanged, resolveAttachments, usageFetchers, void 0, poolUsage), {
|
|
7671
8368
|
async speed(sessionId) {
|
|
7672
8369
|
return {
|
|
7673
8370
|
tier: speedBySession.get(sessionId) ?? "standard",
|
|
@@ -7682,6 +8379,63 @@ function apply(ctx, config) {
|
|
|
7682
8379
|
get: () => proxyGetConfig(),
|
|
7683
8380
|
set: (input) => proxySetConfig(input),
|
|
7684
8381
|
test: (payload) => proxyTestConnection(payload.url, payload.proxy)
|
|
8382
|
+
}, {
|
|
8383
|
+
async catalog() {
|
|
8384
|
+
const visible = new Set((await ctx.llm.listProviders()).map((provider) => provider.id));
|
|
8385
|
+
const catalog$1 = [];
|
|
8386
|
+
for (const provider of PROVIDER_IDS) {
|
|
8387
|
+
if (!visible.has(provider)) continue;
|
|
8388
|
+
let models = [];
|
|
8389
|
+
try {
|
|
8390
|
+
models = await ctx.llm.listModels(provider);
|
|
8391
|
+
} catch {
|
|
8392
|
+
continue;
|
|
8393
|
+
}
|
|
8394
|
+
let tierIds = /* @__PURE__ */ new Set();
|
|
8395
|
+
try {
|
|
8396
|
+
const tiers = await poolAdapter?.modelsForProvider(provider);
|
|
8397
|
+
if (tiers !== void 0) tierIds = new Set(tiers.map((tier) => tier.id));
|
|
8398
|
+
} catch {}
|
|
8399
|
+
const views = [];
|
|
8400
|
+
for (const model of models) {
|
|
8401
|
+
if (tierIds.has(model.id)) continue;
|
|
8402
|
+
let info;
|
|
8403
|
+
try {
|
|
8404
|
+
info = await ctx.llm.resolveModelInfo(provider, model.id);
|
|
8405
|
+
} catch {
|
|
8406
|
+
continue;
|
|
8407
|
+
}
|
|
8408
|
+
if (info === void 0) continue;
|
|
8409
|
+
const override = defaultEffortOf(provider, model.id);
|
|
8410
|
+
views.push({
|
|
8411
|
+
id: model.id,
|
|
8412
|
+
name: model.name,
|
|
8413
|
+
efforts: info.reasoning?.efforts.map((effort) => ({
|
|
8414
|
+
id: effort.id,
|
|
8415
|
+
name: effort.name
|
|
8416
|
+
})) ?? [],
|
|
8417
|
+
...override === void 0 ? {} : { configured: override }
|
|
8418
|
+
});
|
|
8419
|
+
}
|
|
8420
|
+
catalog$1.push({
|
|
8421
|
+
provider,
|
|
8422
|
+
models: views
|
|
8423
|
+
});
|
|
8424
|
+
}
|
|
8425
|
+
return catalog$1;
|
|
8426
|
+
},
|
|
8427
|
+
async set(provider, model, effort) {
|
|
8428
|
+
if (effort !== void 0) {
|
|
8429
|
+
let info;
|
|
8430
|
+
try {
|
|
8431
|
+
info = await ctx.llm.resolveModelInfo(provider, model);
|
|
8432
|
+
} catch {}
|
|
8433
|
+
const offered = info?.reasoning?.efforts ?? [];
|
|
8434
|
+
if (offered.length > 0 && !offered.some((entry) => entry.id === effort)) throw new BadRequest(`model ${model} does not advertise a "${effort}" reasoning effort`);
|
|
8435
|
+
}
|
|
8436
|
+
await setDefaultEffort(provider, model, effort);
|
|
8437
|
+
handles.get(provider)?.replace([provider]);
|
|
8438
|
+
}
|
|
7685
8439
|
});
|
|
7686
8440
|
if (claudeTokens !== void 0) {
|
|
7687
8441
|
const tokens = claudeTokens;
|