dsh-usage-state 0.4.4 → 0.4.5

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/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { createHash } from "node:crypto";
4
5
  import z from "@deepseek-ai/schemastery";
5
6
  //#region src/shared/config.ts
6
7
  const DEFAULT_CONFIG = {
@@ -224,6 +225,101 @@ function suggestSourceId(providerId, baseUrl) {
224
225
  return "sub2api";
225
226
  }
226
227
  //#endregion
228
+ //#region src/shared/providers.ts
229
+ /**
230
+ * The scheme+host of a declared endpoint. A DSH provider profile's `baseURL` is an
231
+ * *API* base (`https://api.z.ai/api/paas/v4`), while every adapter here appends its
232
+ * own path to a bare origin, so only the origin is usable as our endpoint.
233
+ */
234
+ function originOf(url) {
235
+ if (url === void 0) return void 0;
236
+ try {
237
+ const parsed = new URL(url.trim());
238
+ return parsed.origin === "null" ? void 0 : parsed.origin;
239
+ } catch {
240
+ return;
241
+ }
242
+ }
243
+ function withTarget(resolution) {
244
+ if (resolution.sourceId === null || resolution.mode === null) return resolution;
245
+ return {
246
+ ...resolution,
247
+ key: `${resolution.sourceId}:${resolution.mode}`
248
+ };
249
+ }
250
+ /**
251
+ * Resolve one provider. `auto` picks the source the provider id (or its endpoint)
252
+ * suggests and that source's primary mode, so a provider DSH already has a key
253
+ * for starts working without touching the settings page.
254
+ */
255
+ function resolveProvider(input) {
256
+ const entry = input.config.providers[input.provider];
257
+ const sourceId = entry?.sourceId ?? suggestSourceId(input.provider, input.endpointHint) ?? null;
258
+ const defaults = sourceId === null ? void 0 : input.config.sources[sourceId];
259
+ const declared = originOf(input.endpointHint);
260
+ const explicitBaseUrl = entry?.baseUrl ?? defaults?.baseUrl;
261
+ const baseUrl = explicitBaseUrl ?? declared;
262
+ const apiKeyRef = entry?.apiKeyRef ?? defaults?.apiKeyRef;
263
+ const base = {
264
+ provider: input.provider,
265
+ ...baseUrl === void 0 ? {} : { baseUrl },
266
+ ...explicitBaseUrl === void 0 ? {} : { baseUrlPinned: true },
267
+ ...apiKeyRef === void 0 ? {} : { apiKeyRef }
268
+ };
269
+ if (entry?.mode === "hidden") return {
270
+ ...base,
271
+ sourceId: null,
272
+ mode: null,
273
+ reason: "hidden"
274
+ };
275
+ if (sourceId === null) return {
276
+ ...base,
277
+ sourceId: null,
278
+ mode: null,
279
+ reason: "unknown-source"
280
+ };
281
+ const source = input.catalog.find((candidate) => candidate.id === sourceId);
282
+ if (source === void 0) return {
283
+ ...base,
284
+ sourceId,
285
+ mode: null,
286
+ reason: "unknown-source"
287
+ };
288
+ if (entry?.mode === "api" || entry?.mode === "coding-plan") {
289
+ if (!source.modes.includes(entry.mode)) return {
290
+ ...base,
291
+ sourceId,
292
+ mode: null,
293
+ reason: "unsupported"
294
+ };
295
+ return withTarget({
296
+ ...base,
297
+ sourceId,
298
+ mode: entry.mode,
299
+ reason: "configured"
300
+ });
301
+ }
302
+ if (source.requiresBaseUrl && baseUrl === void 0) return {
303
+ ...base,
304
+ sourceId,
305
+ mode: null,
306
+ reason: "needs-endpoint"
307
+ };
308
+ const mode = source.modes[0];
309
+ if (mode === void 0) return {
310
+ ...base,
311
+ sourceId,
312
+ mode: null,
313
+ reason: "unsupported"
314
+ };
315
+ return withTarget({
316
+ ...base,
317
+ sourceId,
318
+ mode,
319
+ reason: "auto"
320
+ });
321
+ }
322
+ //#endregion
227
323
  //#region src/host/sources/normalize.ts
228
324
  /** Shared normalization helpers for provider payloads. */
229
325
  /** Numbers and numeric strings to a finite number; anything else is absent. */
@@ -309,10 +405,12 @@ function pickBalanceInfo(infos) {
309
405
  for (const raw of infos) {
310
406
  if (raw === null || typeof raw !== "object") continue;
311
407
  const entry = raw;
408
+ const amount = toFiniteNumber(entry.total_balance);
409
+ if (amount === void 0) continue;
312
410
  const granted = toFiniteNumber(entry.granted_balance);
313
411
  const toppedUp = toFiniteNumber(entry.topped_up_balance);
314
412
  entries.push({
315
- amount: toFiniteNumber(entry.total_balance) ?? 0,
413
+ amount,
316
414
  currency: typeof entry.currency === "string" ? entry.currency : "",
317
415
  ...granted === void 0 ? {} : { granted },
318
416
  ...toppedUp === void 0 ? {} : { toppedUp }
@@ -375,14 +473,12 @@ function firstFinite(candidates) {
375
473
  }
376
474
  }
377
475
  /**
378
- * Moonshot's balance is documented in cents by some sources and in yuan by others,
379
- * and there is no field that disambiguates the two. This mirrors the heuristic used
380
- * by the plugin this project replaces: amounts of 100 or more are read as cents.
381
- * Verified against a real key before trusting the absolute value.
476
+ * Moonshot's official /v1/users/me/balance fields are already in yuan.
477
+ * https://platform.kimi.com/docs/api/balance
478
+ * Amount size never changes the unit; retain the existing two-decimal rounding.
382
479
  */
383
480
  function moonshotBalanceToYuan(value) {
384
- const yuan = value >= 100 ? value / 100 : value;
385
- return Math.round(yuan * 100) / 100;
481
+ return Math.round(value * 100) / 100;
386
482
  }
387
483
  /** Used percentage from an explicit used/limit pair, or by inverting remaining. */
388
484
  function percentFrom(record) {
@@ -1135,8 +1231,8 @@ function providerCredentialRefs(input) {
1135
1231
  }
1136
1232
  /**
1137
1233
  * The endpoint each configured DSH provider declares (`llm-pi-ai` profiles carry a
1138
- * `baseURL`). Used only to sharpen the data-source suggestion for providers whose
1139
- * id says nothing — e.g. a relay pointed at `open.bigmodel.cn` is a z.ai account.
1234
+ * `baseURL`). Its origin also supplies the account request endpoint, and a
1235
+ * sanitized origin is shared with the browser for consistent resolution.
1140
1236
  */
1141
1237
  function providerEndpointHints(piAiSettings) {
1142
1238
  const providers = asRecord(asRecord(piAiSettings)?.providers);
@@ -1206,7 +1302,8 @@ async function fetchPayload(request, deps) {
1206
1302
  try {
1207
1303
  return await response.json();
1208
1304
  } catch (error) {
1209
- throw new SourceError("parse", `invalid JSON: ${messageOf(error)}`);
1305
+ if (error instanceof SyntaxError) throw new SourceError("parse", `invalid JSON: ${messageOf(error)}`);
1306
+ throw new SourceError("network", messageOf(error));
1210
1307
  }
1211
1308
  }
1212
1309
  /** Adapt `readUsage` to the shape `UsageStateStore` expects. */
@@ -1221,16 +1318,25 @@ function createTargetReader(deps) {
1221
1318
  }
1222
1319
  //#endregion
1223
1320
  //#region src/host/refresh.ts
1224
- function toSnapshotError(error) {
1225
- if (error instanceof SourceError) return error.message === "" ? { kind: error.kind } : {
1226
- kind: error.kind,
1227
- detail: error.message
1321
+ function toSnapshotError(error, secret) {
1322
+ const kind = error instanceof SourceError ? error.kind : "unknown";
1323
+ if (!(error instanceof Error)) return { kind };
1324
+ const message = secret === void 0 || secret === "" ? error.message : error.message.replaceAll(secret, "[redacted]");
1325
+ return message === "" ? { kind } : {
1326
+ kind,
1327
+ detail: message
1228
1328
  };
1229
- if (error instanceof Error) return error.message === "" ? { kind: "unknown" } : {
1230
- kind: "unknown",
1231
- detail: error.message
1232
- };
1233
- return { kind: "unknown" };
1329
+ }
1330
+ function targetSignature(target) {
1331
+ return JSON.stringify(target === void 0 ? null : [
1332
+ target.key,
1333
+ target.sourceId,
1334
+ target.mode,
1335
+ target.baseUrl,
1336
+ target.baseUrlPinned === true,
1337
+ target.apiKeyRef,
1338
+ target.preferredRefs
1339
+ ]);
1234
1340
  }
1235
1341
  /**
1236
1342
  * Holds the last known reading of every configured source and decides when to
@@ -1247,23 +1353,56 @@ var UsageStateStore = class {
1247
1353
  deps;
1248
1354
  readings = /* @__PURE__ */ new Map();
1249
1355
  succeededAt = /* @__PURE__ */ new Map();
1356
+ identities = /* @__PURE__ */ new Map();
1357
+ generations = /* @__PURE__ */ new Map();
1358
+ targetSignatures = /* @__PURE__ */ new Map();
1250
1359
  inflight = /* @__PURE__ */ new Map();
1251
1360
  turnEndCancel;
1252
1361
  idleCancel;
1362
+ idleIntervalMs;
1363
+ stopped = false;
1253
1364
  constructor(deps) {
1254
1365
  this.deps = deps;
1255
1366
  }
1367
+ invalidate(key) {
1368
+ this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
1369
+ this.identities.delete(key);
1370
+ this.readings.delete(key);
1371
+ this.succeededAt.delete(key);
1372
+ this.inflight.delete(key);
1373
+ }
1374
+ currentTargets() {
1375
+ const targets = this.deps.targets().map((target) => ({ ...target }));
1376
+ const active = new Set(targets.map((target) => target.key));
1377
+ for (const key of this.targetSignatures.keys()) {
1378
+ if (active.has(key)) continue;
1379
+ this.invalidate(key);
1380
+ this.targetSignatures.delete(key);
1381
+ }
1382
+ for (const target of targets) {
1383
+ const signature = targetSignature(target);
1384
+ const previous = this.targetSignatures.get(target.key);
1385
+ if (previous !== void 0 && previous !== signature) this.invalidate(target.key);
1386
+ this.targetSignatures.set(target.key, signature);
1387
+ }
1388
+ return targets;
1389
+ }
1256
1390
  /** Everything known so far, keyed by target key. */
1257
1391
  snapshots() {
1392
+ if (this.stopped) return {};
1393
+ this.currentTargets();
1258
1394
  const result = {};
1259
1395
  for (const [key, snapshot] of this.readings) result[key] = snapshot;
1260
1396
  return result;
1261
1397
  }
1262
1398
  snapshot(key) {
1399
+ if (this.stopped) return void 0;
1400
+ this.currentTargets();
1263
1401
  return this.readings.get(key);
1264
1402
  }
1265
1403
  /** Refresh every configured target; failures are captured, never thrown. */
1266
1404
  async refreshAll() {
1405
+ if (this.stopped) return;
1267
1406
  await Promise.all(this.deps.targets().map((target) => this.refresh(target.key)));
1268
1407
  }
1269
1408
  /**
@@ -1271,45 +1410,83 @@ var UsageStateStore = class {
1271
1410
  * not elapsed yet, unless `force` is set.
1272
1411
  */
1273
1412
  async refresh(key, options = {}) {
1274
- const target = this.deps.targets().find((candidate) => candidate.key === key);
1275
- if (target === void 0) return this.fail({
1276
- key,
1413
+ if (this.stopped) return {
1277
1414
  sourceId: key.split(":")[0] ?? "",
1278
- mode: "api"
1279
- }, {
1280
- kind: "config",
1281
- detail: `unknown target ${key}`
1282
- }, this.readings.get(key));
1283
- const cached = this.readings.get(key);
1284
- const lastSuccess = this.succeededAt.get(key);
1285
- if (options.force !== true && cached !== void 0 && cached.error === void 0 && lastSuccess !== void 0 && this.deps.clock.now() - lastSuccess < this.deps.policy().minIntervalSeconds * 1e3) return cached;
1286
- const running = this.inflight.get(key);
1287
- if (running !== void 0) return running;
1288
- const promise = this.run(target).finally(() => {
1289
- this.inflight.delete(key);
1290
- });
1291
- this.inflight.set(key, promise);
1292
- return promise;
1293
- }
1294
- async run(target) {
1295
- const previous = this.readings.get(target.key);
1415
+ mode: key.endsWith(":coding-plan") ? "coding-plan" : "api",
1416
+ balances: [],
1417
+ windows: [],
1418
+ fetchedAt: this.deps.clock.now(),
1419
+ error: {
1420
+ kind: "config",
1421
+ detail: "refresh stopped"
1422
+ }
1423
+ };
1424
+ const target = this.currentTargets().find((candidate) => candidate.key === key);
1425
+ if (target === void 0) return {
1426
+ sourceId: key.split(":")[0] ?? "",
1427
+ mode: key.endsWith(":coding-plan") ? "coding-plan" : "api",
1428
+ balances: [],
1429
+ windows: [],
1430
+ fetchedAt: this.deps.clock.now(),
1431
+ error: {
1432
+ kind: "config",
1433
+ detail: `unknown target ${key}`
1434
+ }
1435
+ };
1436
+ const signature = targetSignature(target);
1437
+ const generationAtLookup = this.generations.get(key) ?? 0;
1296
1438
  const source = this.deps.findSource(target.sourceId);
1297
1439
  if (source === void 0) return this.fail(target, {
1298
1440
  kind: "config",
1299
1441
  detail: `unknown source ${target.sourceId}`
1300
- }, previous);
1442
+ }, this.readings.get(key));
1301
1443
  let credential;
1302
1444
  try {
1303
1445
  credential = await this.deps.credentials.resolve(target);
1304
1446
  } catch (error) {
1305
- return this.fail(target, toSnapshotError(error), previous);
1447
+ if (this.stopped) return this.refresh(key, options);
1448
+ if (signature !== targetSignature(this.currentTargets().find((candidate) => candidate.key === key)) || (this.generations.get(key) ?? 0) !== generationAtLookup) return this.refresh(key, options);
1449
+ return this.fail(target, toSnapshotError(error), this.readings.get(key));
1450
+ }
1451
+ if (this.stopped) return this.refresh(key, options);
1452
+ if (signature !== targetSignature(this.currentTargets().find((candidate) => candidate.key === key))) return this.refresh(key, options);
1453
+ const identity = createHash("sha256").update(JSON.stringify([
1454
+ target.sourceId,
1455
+ target.mode,
1456
+ credential?.baseUrl ?? target.baseUrl ?? source.defaultBaseUrl(target.mode),
1457
+ credential?.baseUrlPinned === true || target.baseUrlPinned === true,
1458
+ credential?.ref ?? target.apiKeyRef,
1459
+ credential?.apiKey
1460
+ ])).digest("hex");
1461
+ if ((this.generations.get(key) ?? 0) !== generationAtLookup && this.identities.get(key) !== identity) return this.refresh(key, options);
1462
+ if (this.identities.get(key) !== identity) {
1463
+ this.identities.set(key, identity);
1464
+ this.generations.set(key, (this.generations.get(key) ?? 0) + 1);
1465
+ this.readings.delete(key);
1466
+ this.succeededAt.delete(key);
1467
+ this.inflight.delete(key);
1306
1468
  }
1307
1469
  if (credential === void 0) return this.fail(target, {
1308
1470
  kind: "config",
1309
1471
  detail: "no credential configured"
1310
- }, previous);
1472
+ }, this.readings.get(key));
1473
+ const cached = this.readings.get(key);
1474
+ const lastSuccess = this.succeededAt.get(key);
1475
+ if (options.force !== true && cached !== void 0 && (options.onlyIfMissing === true || cached.error === void 0 && lastSuccess !== void 0 && this.deps.clock.now() - lastSuccess < this.deps.policy().minIntervalSeconds * 1e3)) return cached;
1476
+ const running = this.inflight.get(key);
1477
+ if (running !== void 0) return running;
1478
+ const generation = this.generations.get(key) ?? 0;
1479
+ const promise = this.run(target, credential, source, generation).finally(() => {
1480
+ if (this.inflight.get(key) === promise) this.inflight.delete(key);
1481
+ });
1482
+ this.inflight.set(key, promise);
1483
+ return promise;
1484
+ }
1485
+ async run(target, credential, source, generation) {
1486
+ const previous = this.readings.get(target.key);
1311
1487
  try {
1312
1488
  const reading = await this.deps.read(target, credential, source);
1489
+ if (this.generations.get(target.key) !== generation) return this.refresh(target.key);
1313
1490
  const fetchedAt = this.deps.clock.now();
1314
1491
  const snapshot = {
1315
1492
  sourceId: target.sourceId,
@@ -1322,7 +1499,8 @@ var UsageStateStore = class {
1322
1499
  this.succeededAt.set(target.key, fetchedAt);
1323
1500
  return snapshot;
1324
1501
  } catch (error) {
1325
- return this.fail(target, toSnapshotError(error), previous);
1502
+ if (this.generations.get(target.key) !== generation) return this.refresh(target.key);
1503
+ return this.fail(target, toSnapshotError(error, credential.apiKey), previous);
1326
1504
  }
1327
1505
  }
1328
1506
  fail(target, error, previous) {
@@ -1353,10 +1531,22 @@ var UsageStateStore = class {
1353
1531
  });
1354
1532
  this.turnEndCancel = cancel;
1355
1533
  }
1534
+ /** Apply a live policy edit without leaving the previous idle timer running. */
1535
+ reconfigure() {
1536
+ if (this.stopped) return;
1537
+ this.currentTargets();
1538
+ if (this.idleCancel === void 0) return;
1539
+ if (this.idleIntervalMs === this.deps.policy().intervalMinutes * 6e4) return;
1540
+ this.idleCancel();
1541
+ this.idleCancel = void 0;
1542
+ this.start();
1543
+ }
1356
1544
  /** Start the idle fallback timer; returns a stop function. */
1357
1545
  start() {
1546
+ this.stopped = false;
1358
1547
  if (this.idleCancel === void 0) {
1359
1548
  const intervalMs = this.deps.policy().intervalMinutes * 6e4;
1549
+ this.idleIntervalMs = intervalMs;
1360
1550
  this.idleCancel = this.deps.clock.every(intervalMs, () => {
1361
1551
  this.refreshAll();
1362
1552
  });
@@ -1364,6 +1554,8 @@ var UsageStateStore = class {
1364
1554
  return () => this.stop();
1365
1555
  }
1366
1556
  stop() {
1557
+ this.stopped = true;
1558
+ for (const key of this.targetSignatures.keys()) this.invalidate(key);
1367
1559
  this.turnEndCancel?.();
1368
1560
  this.turnEndCancel = void 0;
1369
1561
  this.idleCancel?.();
@@ -1387,18 +1579,20 @@ var UsageStateService = class {
1387
1579
  this.deps = deps;
1388
1580
  }
1389
1581
  /**
1390
- * Refresh what is due and report every reading. Forcing bypasses the minimum
1391
- * interval, so it is reserved for the explicit "refresh now" affordance.
1582
+ * Initialize a missing account identity, otherwise report cached readings.
1583
+ * The host owns periodic/turn refreshes; polling this method cannot shorten
1584
+ * that cadence. Forcing is reserved for an explicit refresh.
1392
1585
  *
1393
1586
  * Settled rather than all: one unreachable provider must not blank the whole
1394
1587
  * status line for the others.
1395
1588
  */
1396
1589
  async getState(force) {
1397
- const options = force === true ? { force: true } : {};
1590
+ const options = force === true ? { force: true } : { onlyIfMissing: true };
1398
1591
  await Promise.allSettled(this.deps.targets().map((target) => this.deps.store.refresh(target.key, options)));
1399
1592
  return {
1400
1593
  sources: this.deps.catalog(),
1401
1594
  snapshots: this.deps.store.snapshots(),
1595
+ ...this.deps.endpointHints === void 0 ? {} : { endpointHints: this.deps.endpointHints() },
1402
1596
  checkedAt: this.deps.now()
1403
1597
  };
1404
1598
  }
@@ -1450,98 +1644,6 @@ function installUsageStateSettings(ctx, reference, onConfig) {
1450
1644
  });
1451
1645
  }
1452
1646
  //#endregion
1453
- //#region src/shared/providers.ts
1454
- /**
1455
- * The scheme+host of a declared endpoint. A DSH provider profile's `baseURL` is an
1456
- * *API* base (`https://api.z.ai/api/paas/v4`), while every adapter here appends its
1457
- * own path to a bare origin, so only the origin is usable as our endpoint.
1458
- */
1459
- function originOf(url) {
1460
- if (url === void 0) return void 0;
1461
- try {
1462
- const parsed = new URL(url.trim());
1463
- return parsed.origin === "null" ? void 0 : parsed.origin;
1464
- } catch {
1465
- return;
1466
- }
1467
- }
1468
- function withTarget(resolution) {
1469
- if (resolution.sourceId === null || resolution.mode === null) return resolution;
1470
- return {
1471
- ...resolution,
1472
- key: `${resolution.sourceId}:${resolution.mode}`
1473
- };
1474
- }
1475
- /**
1476
- * Resolve one provider. `auto` picks the source the provider id (or its endpoint)
1477
- * suggests and that source's primary mode, so a provider DSH already has a key
1478
- * for starts working without touching the settings page.
1479
- */
1480
- function resolveProvider(input) {
1481
- const entry = input.config.providers[input.provider];
1482
- const declared = originOf(input.endpointHint);
1483
- const baseUrl = entry?.baseUrl ?? declared;
1484
- const base = {
1485
- provider: input.provider,
1486
- ...baseUrl === void 0 ? {} : { baseUrl },
1487
- ...entry?.baseUrl === void 0 ? {} : { baseUrlPinned: true },
1488
- ...entry?.apiKeyRef === void 0 ? {} : { apiKeyRef: entry.apiKeyRef }
1489
- };
1490
- if (entry?.mode === "hidden") return {
1491
- ...base,
1492
- sourceId: null,
1493
- mode: null,
1494
- reason: "hidden"
1495
- };
1496
- const sourceId = entry?.sourceId ?? suggestSourceId(input.provider, input.endpointHint) ?? null;
1497
- if (sourceId === null) return {
1498
- ...base,
1499
- sourceId: null,
1500
- mode: null,
1501
- reason: "unknown-source"
1502
- };
1503
- const source = input.catalog.find((candidate) => candidate.id === sourceId);
1504
- if (source === void 0) return {
1505
- ...base,
1506
- sourceId,
1507
- mode: null,
1508
- reason: "unknown-source"
1509
- };
1510
- if (entry?.mode === "api" || entry?.mode === "coding-plan") {
1511
- if (!source.modes.includes(entry.mode)) return {
1512
- ...base,
1513
- sourceId,
1514
- mode: null,
1515
- reason: "unsupported"
1516
- };
1517
- return withTarget({
1518
- ...base,
1519
- sourceId,
1520
- mode: entry.mode,
1521
- reason: "configured"
1522
- });
1523
- }
1524
- if (source.requiresBaseUrl && baseUrl === void 0) return {
1525
- ...base,
1526
- sourceId,
1527
- mode: null,
1528
- reason: "needs-endpoint"
1529
- };
1530
- const mode = source.modes[0];
1531
- if (mode === void 0) return {
1532
- ...base,
1533
- sourceId,
1534
- mode: null,
1535
- reason: "unsupported"
1536
- };
1537
- return withTarget({
1538
- ...base,
1539
- sourceId,
1540
- mode,
1541
- reason: "auto"
1542
- });
1543
- }
1544
- //#endregion
1545
1647
  //#region src/host/targets.ts
1546
1648
  function targetKey(sourceId, mode) {
1547
1649
  return `${sourceId}:${mode}`;
@@ -1554,7 +1656,8 @@ function targetKey(sourceId, mode) {
1554
1656
  * line never shows the same account twice. Providers that are hidden, map to no
1555
1657
  * known source, or ask for a mode the source cannot serve are skipped silently.
1556
1658
  */
1557
- function resolveTargets(config, options = {}) {
1659
+ function resolveTargets(rawConfig, options = {}) {
1660
+ const config = normalizeConfig(rawConfig);
1558
1661
  const sources = options.sources ?? ALL_SOURCES;
1559
1662
  const catalog = toSourceCatalog(sources);
1560
1663
  const targets = [];
@@ -1570,26 +1673,21 @@ function resolveTargets(config, options = {}) {
1570
1673
  sourceId,
1571
1674
  mode,
1572
1675
  ...endpoint?.baseUrl === void 0 ? {} : { baseUrl: endpoint.baseUrl },
1573
- ...endpoint?.pinned === true ? { baseUrlPinned: true } : {}
1676
+ ...endpoint?.pinned === true ? { baseUrlPinned: true } : {},
1677
+ ...endpoint?.apiKeyRef === void 0 ? {} : { apiKeyRef: endpoint.apiKeyRef }
1574
1678
  });
1575
1679
  };
1576
1680
  /**
1577
- * The live runtime's provider ids, when we have them. An empty list means "could
1578
- * not ask" (the `llm` service is missing on early ticks), not "nothing exists", so
1579
- * it must not suppress the configured entries.
1681
+ * The live runtime's provider ids, when known. `undefined` means the host
1682
+ * could not answer; an empty list is authoritative and stops stale targets.
1580
1683
  */
1581
- const live = options.providers !== void 0 && options.providers.length > 0 ? new Set(options.providers) : void 0;
1684
+ const live = options.providers === void 0 ? void 0 : new Set(options.providers);
1582
1685
  /**
1583
1686
  * Whether a stored entry may still produce requests. The entry itself is kept — a
1584
1687
  * provider that comes back keeps the mode the user chose for it — but a provider
1585
1688
  * DSH no longer has must not keep an account being polled behind an invisible row.
1586
1689
  */
1587
1690
  const pollable = (provider) => live === void 0 || live.has(provider);
1588
- for (const entry of config.models) {
1589
- if (entry.mode === "hidden" || entry.sourceId === null) continue;
1590
- if (!pollable(entry.provider)) continue;
1591
- push(entry.sourceId, entry.mode);
1592
- }
1593
1691
  const providers = /* @__PURE__ */ new Set();
1594
1692
  for (const provider of [...config.order, ...Object.keys(config.providers)]) if (pollable(provider)) providers.add(provider);
1595
1693
  for (const provider of options.providers ?? []) providers.add(provider);
@@ -1602,7 +1700,8 @@ function resolveTargets(config, options = {}) {
1602
1700
  });
1603
1701
  if (resolution.sourceId !== null && resolution.mode !== null) push(resolution.sourceId, resolution.mode, {
1604
1702
  ...resolution.baseUrl === void 0 ? {} : { baseUrl: resolution.baseUrl },
1605
- ...resolution.baseUrlPinned === true ? { pinned: true } : {}
1703
+ ...resolution.baseUrlPinned === true ? { pinned: true } : {},
1704
+ ...resolution.apiKeyRef === void 0 ? {} : { apiKeyRef: resolution.apiKeyRef }
1606
1705
  });
1607
1706
  }
1608
1707
  return targets;
@@ -1716,8 +1815,10 @@ function createUsageState(ctx, deps = {}) {
1716
1815
  const catalog = toSourceCatalog(ALL_SOURCES);
1717
1816
  const lookup = createCredentialLookup(ctx, deps.credentialFallback ?? {});
1718
1817
  let config = DEFAULT_CONFIG;
1818
+ let reconfigure = () => {};
1719
1819
  installUsageStateSettings(ctx, deps.config, (next) => {
1720
1820
  config = next;
1821
+ reconfigure();
1721
1822
  });
1722
1823
  /**
1723
1824
  * The plugin's own configuration says nothing about which providers exist, so
@@ -1725,11 +1826,16 @@ function createUsageState(ctx, deps = {}) {
1725
1826
  * provider nobody configured still resolves to its suggested source.
1726
1827
  */
1727
1828
  const providerFacts = () => {
1728
- const llm = ctx.get("llm");
1729
- const ids = [];
1829
+ let ids;
1730
1830
  try {
1731
- for (const provider of llm?.listProviders?.() ?? []) if (typeof provider?.id === "string" && provider.id !== "") ids.push(provider.id);
1732
- } catch {}
1831
+ const llm = ctx.get("llm");
1832
+ if (typeof llm?.listProviders === "function") {
1833
+ ids = [];
1834
+ for (const provider of llm.listProviders()) if (typeof provider?.id === "string" && provider.id !== "") ids.push(provider.id);
1835
+ }
1836
+ } catch {
1837
+ ids = void 0;
1838
+ }
1733
1839
  return {
1734
1840
  ids,
1735
1841
  hints: providerEndpointHints(readNamespace(ctx, "llm-pi-ai"))
@@ -1740,17 +1846,20 @@ function createUsageState(ctx, deps = {}) {
1740
1846
  return resolveTargets(config, {
1741
1847
  providers: ids,
1742
1848
  endpointHints: hints
1743
- });
1744
- };
1745
- const optionsFor = (target) => {
1746
- const overrideRef = config.sources[target.sourceId]?.apiKeyRef;
1747
- return {
1748
- ...overrideRef === void 0 ? {} : { overrideRef },
1849
+ }).map((target) => ({
1850
+ ...target,
1749
1851
  preferredRefs: providerCredentialRefs({
1750
1852
  sourceId: target.sourceId,
1751
1853
  deepseekSettings: readNamespace(ctx, "llm-deepseek"),
1752
1854
  piAiSettings: readNamespace(ctx, "llm-pi-ai")
1753
1855
  })
1856
+ }));
1857
+ };
1858
+ const optionsFor = (target) => {
1859
+ const overrideRef = target.apiKeyRef;
1860
+ return {
1861
+ ...overrideRef === void 0 ? {} : { overrideRef },
1862
+ preferredRefs: target.preferredRefs ?? []
1754
1863
  };
1755
1864
  };
1756
1865
  const store = new UsageStateStore({
@@ -1767,21 +1876,24 @@ function createUsageState(ctx, deps = {}) {
1767
1876
  if (source === void 0) return void 0;
1768
1877
  const resolved = await resolveApiKey(source, target.mode, optionsFor(target), lookup);
1769
1878
  if (resolved === void 0) return void 0;
1770
- const pinnedBaseUrl = config.sources[target.sourceId]?.baseUrl;
1771
- const baseUrl = pinnedBaseUrl ?? target.baseUrl;
1772
- if (baseUrl === void 0) return resolved;
1879
+ if (target.baseUrl === void 0) return resolved;
1773
1880
  return {
1774
1881
  ...resolved,
1775
- baseUrl,
1776
- ...pinnedBaseUrl === void 0 ? {} : { baseUrlPinned: true }
1882
+ baseUrl: target.baseUrl,
1883
+ ...target.baseUrlPinned === true ? { baseUrlPinned: true } : {}
1777
1884
  };
1778
1885
  } },
1779
1886
  read: createTargetReader({ fetch: fetchImpl })
1780
1887
  });
1888
+ reconfigure = () => store.reconfigure();
1781
1889
  const service = new UsageStateService({
1782
1890
  store,
1783
1891
  targets,
1784
1892
  catalog: () => catalog,
1893
+ endpointHints: () => Object.fromEntries(Object.entries(providerFacts().hints).flatMap(([provider, endpoint]) => {
1894
+ const origin = originOf(endpoint);
1895
+ return origin === void 0 ? [] : [[provider, origin]];
1896
+ })),
1785
1897
  describe: async (target) => {
1786
1898
  const source = findSource(target.sourceId);
1787
1899
  if (source === void 0) return {
@@ -1795,7 +1907,7 @@ function createUsageState(ctx, deps = {}) {
1795
1907
  ctx.on("session/event", (...args) => {
1796
1908
  if (isTurnEnd(args[1])) store.onTurnEnd();
1797
1909
  });
1798
- store.start();
1910
+ ctx.effect(() => store.start(), "dsh-usage-state: refresh scheduling");
1799
1911
  return provideUsageState(ctx, service);
1800
1912
  }
1801
1913
  /**
package/lib/typert.js CHANGED
@@ -80,6 +80,7 @@ const credentialDescriptionSchema = z.object({
80
80
  const usageStateViewSchema = z.object({
81
81
  sources: z.array(catalogEntrySchema),
82
82
  snapshots: z.record(z.string(), snapshotSchema),
83
+ endpointHints: z.record(z.string(), z.string()).optional(),
83
84
  checkedAt: z.number()
84
85
  });
85
86
  const credentialReportSchema = z.object({ credentials: z.record(z.string(), credentialDescriptionSchema) });