dsh-subscriptions 0.1.1 → 0.1.3

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.
Files changed (41) hide show
  1. package/README.md +115 -4
  2. package/icon.svg +28 -0
  3. package/lib/client/CursorCard.js +15 -11
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +28 -13
  6. package/lib/client/SubscriptionUsageBadge.d.ts +25 -4
  7. package/lib/client/SubscriptionUsageBadge.js +115 -28
  8. package/lib/client/SubscriptionsSection.d.ts +7 -2
  9. package/lib/client/SubscriptionsSection.js +9 -13
  10. package/lib/client/UsageBadgeDisplaySetting.js +19 -2
  11. package/lib/client/UsageMeter.d.ts +8 -0
  12. package/lib/client/UsageMeter.js +56 -0
  13. package/lib/client/host-contract.d.ts +1 -1
  14. package/lib/client/host-contract.js +1 -0
  15. package/lib/client/locales.d.ts +50 -0
  16. package/lib/client/locales.js +52 -2
  17. package/lib/client/subscription-card-styles.js +0 -5
  18. package/lib/client/usage-badge-preferences.d.ts +7 -2
  19. package/lib/client/usage-badge-preferences.js +3 -2
  20. package/lib/client/usage-color-preferences.d.ts +6 -0
  21. package/lib/client/usage-color-preferences.js +38 -0
  22. package/lib/client/usage-pace.d.ts +42 -0
  23. package/lib/client/usage-pace.js +71 -0
  24. package/lib/client.js +489 -132
  25. package/lib/client.js.map +1 -1
  26. package/lib/index.js +252 -35
  27. package/lib/providers/claude.js +7 -0
  28. package/lib/providers/codex.js +5 -0
  29. package/lib/providers/common.d.ts +10 -0
  30. package/lib/providers/cursor-usage.js +6 -2
  31. package/lib/providers/external-usage-controller.d.ts +1 -1
  32. package/lib/providers/external-usage-controller.js +8 -3
  33. package/lib/providers/external-usage.d.ts +1 -1
  34. package/lib/providers/external-usage.js +62 -2
  35. package/lib/providers/grok.js +3 -1
  36. package/lib/providers/minimax-usage.d.ts +24 -0
  37. package/lib/providers/minimax-usage.js +85 -0
  38. package/lib/providers/pool-usage.js +6 -4
  39. package/locale/en.json +6 -0
  40. package/locale/zh.json +6 -0
  41. package/package.json +7 -3
package/lib/index.js CHANGED
@@ -1031,8 +1031,121 @@ async function setDefaultAccount(provider, account, path = authFilePath()) {
1031
1031
  });
1032
1032
  }
1033
1033
 
1034
+ //#endregion
1035
+ //#region src/providers/minimax-usage.ts
1036
+ /**
1037
+ * Used percentage from the legacy counts, read as the official CLI does when no
1038
+ * explicit percentage exists: the count is the quota remaining. Undefined
1039
+ * unless the total is a positive finite number, so a zero total never yields a
1040
+ * bar.
1041
+ */
1042
+ function usedFromCounts(total, count) {
1043
+ return typeof total === "number" && Number.isFinite(total) && total > 0 && typeof count === "number" ? (1 - count / total) * 100 : void 0;
1044
+ }
1045
+ /**
1046
+ * Read MiniMax subscription quota (M Plan and Token Plan), not the balance API
1047
+ * that `sk-api-*` pay-as-you-go keys use.
1048
+ *
1049
+ * Each `model_remains` row can yield an interval window and a weekly window.
1050
+ * A window needs a finite share: the explicit `*_remaining_percent` (the weekly
1051
+ * one scaled by `weekly_boost_permille`), else the legacy counts. Time-based
1052
+ * plans report zero counts beside the percentage for standard models, so the
1053
+ * percentage stands on its own and a zero total does not hide the window.
1054
+ * Windows that cannot be a finite bar are omitted rather than approximated:
1055
+ * status 3 (unlimited, or no quota in the plan), a weekly pool boosted past
1056
+ * 100% remaining, a percentage outside 0-100, and a row with neither a
1057
+ * percentage nor positive counts. Start and end times are kept only when both
1058
+ * are valid millisecond bounds.
1059
+ * @param key - subscription key for the region; never copied into a result or error.
1060
+ * @param region - `global` or `cn`, which selects the fixed quota host.
1061
+ * @param http - fetch implementation (injectable for tests).
1062
+ * @param signal - caller cancellation from the RPC transport.
1063
+ * @returns the finite windows in the order the API lists them.
1064
+ * @throws when the key is pay-as-you-go, the endpoint or its business status
1065
+ * fails, the body has no `model_remains`, or no window is a finite bar.
1066
+ */
1067
+ async function fetchMiniMaxUsage(key, region, http$1 = fetch, signal) {
1068
+ if (key.startsWith("sk-api-")) throw new Error("MiniMax requires a subscription key, not a pay-as-you-go key");
1069
+ const response = await http$1(`https://www.minimax.${region === "cn" ? "cn" : "io"}/v1/token_plan/remains`, {
1070
+ headers: {
1071
+ authorization: `Bearer ${key}`,
1072
+ "content-type": "application/json"
1073
+ },
1074
+ ...signal === void 0 ? {} : { signal }
1075
+ });
1076
+ if (!response.ok) throw new Error(`MiniMax usage endpoint returned HTTP ${response.status}`);
1077
+ const body = await response.json();
1078
+ if (body?.base_resp?.status_code !== void 0 && body.base_resp.status_code !== 0 || !Array.isArray(body?.model_remains)) throw new Error("MiniMax usage response failed or has no quota records");
1079
+ const windows = [];
1080
+ for (const raw of body.model_remains) {
1081
+ if (raw === null || typeof raw !== "object") continue;
1082
+ const row = raw;
1083
+ if (typeof row.model_name !== "string") continue;
1084
+ for (const [prefix, startKey, endKey, fallback] of [[
1085
+ "current_interval",
1086
+ "start_time",
1087
+ "end_time",
1088
+ "session"
1089
+ ], [
1090
+ "current_weekly",
1091
+ "weekly_start_time",
1092
+ "weekly_end_time",
1093
+ "weekly"
1094
+ ]]) {
1095
+ if (row[`${prefix}_status`] === 3) continue;
1096
+ const remaining = row[`${prefix}_remaining_percent`];
1097
+ const boost = prefix === "current_weekly" ? row.weekly_boost_permille ?? 1e3 : 1e3;
1098
+ if (typeof boost !== "number" || !Number.isFinite(boost) || boost <= 0) continue;
1099
+ const usedPercent = typeof remaining === "number" ? 100 - remaining * boost / 1e3 : usedFromCounts(row[`${prefix}_total_count`], row[`${prefix}_usage_count`]);
1100
+ if (usedPercent === void 0 || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) continue;
1101
+ const start = row[startKey];
1102
+ const end = row[endKey];
1103
+ const validBounds = typeof start === "number" && Number.isFinite(start) && start > 0 && typeof end === "number" && Number.isFinite(end) && end > start;
1104
+ windows.push({
1105
+ kind: fallback === "session" && validBounds && end - start !== 18e6 ? "other" : fallback,
1106
+ scope: row.model_name,
1107
+ usedPercent,
1108
+ ...validBounds ? {
1109
+ startsAt: start,
1110
+ resetsAt: end
1111
+ } : {}
1112
+ });
1113
+ }
1114
+ }
1115
+ if (!windows.length) throw new Error("MiniMax has no supported finite quota windows");
1116
+ return {
1117
+ supported: true,
1118
+ plan: "MiniMax",
1119
+ windows
1120
+ };
1121
+ }
1122
+
1034
1123
  //#endregion
1035
1124
  //#region src/providers/external-usage.ts
1125
+ function numeric(value) {
1126
+ const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
1127
+ return typeof n === "number" && Number.isFinite(n) ? n : void 0;
1128
+ }
1129
+ function kimiReset(value) {
1130
+ for (const key of [
1131
+ "reset_at",
1132
+ "resetAt",
1133
+ "reset_time",
1134
+ "resetTime"
1135
+ ]) {
1136
+ const reset = resetTime$1(value[key]);
1137
+ if (reset !== void 0) return reset;
1138
+ }
1139
+ for (const key of [
1140
+ "reset_in",
1141
+ "resetIn",
1142
+ "ttl",
1143
+ "window"
1144
+ ]) {
1145
+ const seconds = numeric(value[key]);
1146
+ if (seconds !== void 0 && seconds > 0) return Date.now() + seconds * 1e3;
1147
+ }
1148
+ }
1036
1149
  function record$3(value) {
1037
1150
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1038
1151
  }
@@ -1085,6 +1198,13 @@ async function fetchOpenCodeGoUsage(apiKey, http$1 = fetch, signal) {
1085
1198
  kind,
1086
1199
  ...scope === void 0 ? {} : { scope },
1087
1200
  usedPercent,
1201
+ ...field === "rolling" ? {
1202
+ fixedWindow: true,
1203
+ windowDurationMs: 18e6
1204
+ } : field === "weekly" ? {
1205
+ fixedWindow: true,
1206
+ windowDurationMs: 6048e5
1207
+ } : {},
1088
1208
  ...resetsAt === void 0 ? {} : { resetsAt }
1089
1209
  });
1090
1210
  }
@@ -1095,12 +1215,45 @@ async function fetchOpenCodeGoUsage(apiKey, http$1 = fetch, signal) {
1095
1215
  plan: "OpenCode Go"
1096
1216
  };
1097
1217
  }
1098
- /** Read the Kimi Code plan windows using the same API as the Kimi Code CLI. */
1218
+ /** Read official CLI count windows and retain compatibility with older ratio pools. */
1099
1219
  async function fetchKimiCodeUsage(apiKey, http$1 = fetch, signal) {
1100
1220
  const body = record$3(await getUsage("https://api.kimi.com/coding/v1/usages", apiKey, http$1, signal));
1101
1221
  if (body === void 0) throw new Error("Kimi Code usage response is not an object");
1222
+ const modern = [];
1223
+ const rows = [...record$3(body.usage) ? [{ detail: body.usage }] : [], ...Array.isArray(body.limits) ? body.limits : []];
1224
+ for (const raw of rows) {
1225
+ const item = record$3(raw);
1226
+ if (!item) continue;
1227
+ const detail = record$3(item.detail) ?? item;
1228
+ const limit = numeric(detail.limit);
1229
+ const used = numeric(detail.used) ?? (limit !== void 0 && numeric(detail.remaining) !== void 0 ? limit - numeric(detail.remaining) : void 0);
1230
+ if (limit === void 0 || limit <= 0 || used === void 0) continue;
1231
+ const usedPercent = percent(used / limit * 100);
1232
+ if (usedPercent === void 0) continue;
1233
+ const window = record$3(item.window) ?? item;
1234
+ const duration = numeric(window.duration ?? item.duration ?? detail.duration);
1235
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
1236
+ const multiplier = unit.includes("MINUTE") ? 6e4 : unit.includes("HOUR") ? 36e5 : unit.includes("DAY") ? 864e5 : unit.includes("SECOND") ? 1e3 : void 0;
1237
+ const durationMs$1 = duration === void 0 || duration <= 0 || multiplier === void 0 ? void 0 : duration * multiplier;
1238
+ const kind = durationMs$1 === 18e6 ? "session" : durationMs$1 === 6048e5 ? "weekly" : "other";
1239
+ const resetsAt = kimiReset(detail);
1240
+ const scope = detail.name ?? detail.title ?? item.name ?? item.title;
1241
+ modern.push({
1242
+ kind,
1243
+ usedPercent,
1244
+ ...typeof scope === "string" ? { scope } : {},
1245
+ ...resetsAt === void 0 ? {} : { resetsAt },
1246
+ ...durationMs$1 === void 0 ? {} : { windowDurationMs: durationMs$1 },
1247
+ ...kind === "weekly" ? { fixedWindow: true } : {}
1248
+ });
1249
+ }
1250
+ if (modern.length) return {
1251
+ supported: true,
1252
+ windows: modern,
1253
+ plan: "Kimi Code"
1254
+ };
1102
1255
  const usages = record$3(body.usages);
1103
- if (usages === void 0) throw new Error("Kimi Code usage response has no usages object");
1256
+ if (usages === void 0) throw new Error("Kimi Code usage response has no valid windows");
1104
1257
  const windows = [];
1105
1258
  for (const [field, kind, scope] of [
1106
1259
  [
@@ -1127,6 +1280,10 @@ async function fetchKimiCodeUsage(apiKey, http$1 = fetch, signal) {
1127
1280
  kind,
1128
1281
  ...scope === void 0 ? {} : { scope },
1129
1282
  usedPercent: ratio * 100,
1283
+ ...field === "limit_7d" ? {
1284
+ fixedWindow: true,
1285
+ windowDurationMs: 6048e5
1286
+ } : {},
1130
1287
  ...resetsAt === void 0 ? {} : { resetsAt }
1131
1288
  });
1132
1289
  }
@@ -1139,10 +1296,17 @@ async function fetchKimiCodeUsage(apiKey, http$1 = fetch, signal) {
1139
1296
 
1140
1297
  //#endregion
1141
1298
  //#region src/providers/external-usage-controller.ts
1142
- const EXTERNAL_USAGE_SOURCES = ["opencode-go", "kimi-code"];
1299
+ const EXTERNAL_USAGE_SOURCES = [
1300
+ "opencode-go",
1301
+ "kimi-code",
1302
+ "minimax",
1303
+ "minimax-cn"
1304
+ ];
1143
1305
  const DEFAULT_REFS = {
1144
1306
  "opencode-go": "OPENCODE_GO_API_KEY",
1145
- "kimi-code": "KIMI_CODING_API_KEY"
1307
+ "kimi-code": "KIMI_CODING_API_KEY",
1308
+ "minimax": "MINIMAX_API_KEY",
1309
+ "minimax-cn": "MINIMAX_CN_API_KEY"
1146
1310
  };
1147
1311
  var ExternalUsageController = class {
1148
1312
  refs;
@@ -1155,18 +1319,16 @@ var ExternalUsageController = class {
1155
1319
  };
1156
1320
  }
1157
1321
  async status() {
1158
- const [go, kimi] = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
1322
+ const values = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
1159
1323
  const value = await this.resolveCredential(this.refs[source]);
1160
1324
  return typeof value?.value === "string" && value.value.length > 0;
1161
1325
  }));
1162
- return {
1163
- "opencode-go": { configured: go ?? false },
1164
- "kimi-code": { configured: kimi ?? false }
1165
- };
1326
+ return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [source, { configured: values[i] ?? false }]));
1166
1327
  }
1167
1328
  async usage(source, signal) {
1168
1329
  const credential = await this.resolveCredential(this.refs[source]);
1169
1330
  if (credential?.value === void 0 || credential.value.length === 0) throw new Error(`${source} API key is not configured`);
1331
+ if (source === "minimax" || source === "minimax-cn") return fetchMiniMaxUsage(credential.value, source === "minimax-cn" ? "cn" : "global", this.http, signal);
1170
1332
  return source === "opencode-go" ? fetchOpenCodeGoUsage(credential.value, this.http, signal) : fetchKimiCodeUsage(credential.value, this.http, signal);
1171
1333
  }
1172
1334
  };
@@ -1766,6 +1928,12 @@ async function fetchCursorUsage(accessToken, http$1 = fetch, signal) {
1766
1928
  const resetValue = summary?.billingCycleEnd;
1767
1929
  const parsedReset = typeof resetValue === "string" ? Date.parse(resetValue) : NaN;
1768
1930
  const resetsAt = Number.isFinite(parsedReset) ? parsedReset : void 0;
1931
+ const startValue = summary?.billingCycleStart;
1932
+ const parsedStart = typeof startValue === "string" ? Date.parse(startValue) : NaN;
1933
+ const timing = {
1934
+ ...resetsAt === void 0 ? {} : { resetsAt },
1935
+ ...Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}
1936
+ };
1769
1937
  const windows = [];
1770
1938
  for (const [field, scope] of [
1771
1939
  ["totalPercentUsed", "Included"],
@@ -1778,11 +1946,14 @@ async function fetchCursorUsage(accessToken, http$1 = fetch, signal) {
1778
1946
  kind: "other",
1779
1947
  scope,
1780
1948
  usedPercent,
1781
- ...resetsAt === void 0 ? {} : { resetsAt }
1949
+ ...timing
1782
1950
  });
1783
1951
  }
1784
1952
  const legacy = legacyRequestWindow(legacyResult.status === "fulfilled" ? legacyResult.value : void 0, resetsAt);
1785
- if (legacy !== void 0) windows.push(legacy);
1953
+ if (legacy !== void 0) windows.push({
1954
+ ...legacy,
1955
+ ...timing
1956
+ });
1786
1957
  if (windows.length === 0 && summary?.isUnlimited !== true) throw new Error("Cursor usage response has no quota windows");
1787
1958
  const membershipType = summary?.membershipType;
1788
1959
  return {
@@ -4521,8 +4692,8 @@ function resetInstantFromValue(value, now) {
4521
4692
  if (typeof value !== "string") return void 0;
4522
4693
  const trimmed = value.trim();
4523
4694
  if (trimmed.length === 0) return void 0;
4524
- const numeric = Number(trimmed);
4525
- if (Number.isFinite(numeric)) return resetInstantFromNumber(numeric, now);
4695
+ const numeric$1 = Number(trimmed);
4696
+ if (Number.isFinite(numeric$1)) return resetInstantFromNumber(numeric$1, now);
4526
4697
  const duration = durationMs(trimmed);
4527
4698
  if (duration !== void 0) return now + duration;
4528
4699
  const parsed = Date.parse(trimmed);
@@ -6548,6 +6719,16 @@ function claudeResetsAt(value) {
6548
6719
  const parsed = Date.parse(value);
6549
6720
  return Number.isFinite(parsed) ? parsed : void 0;
6550
6721
  }
6722
+ /** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
6723
+ function claudeWindowTiming(kind) {
6724
+ return kind === "session" ? {
6725
+ windowDurationMs: 300 * 6e4,
6726
+ fixedWindow: true
6727
+ } : kind === "weekly" ? {
6728
+ windowDurationMs: 10080 * 6e4,
6729
+ fixedWindow: true
6730
+ } : {};
6731
+ }
6551
6732
  /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
6552
6733
  function claudeLegacyWindow(value, kind, scope) {
6553
6734
  if (typeof value !== "object" || value === null) return void 0;
@@ -6557,6 +6738,7 @@ function claudeLegacyWindow(value, kind, scope) {
6557
6738
  return {
6558
6739
  kind,
6559
6740
  ...scope === void 0 ? {} : { scope },
6741
+ ...claudeWindowTiming(kind),
6560
6742
  usedPercent: bucket.utilization,
6561
6743
  ...resetsAt === void 0 ? {} : { resetsAt }
6562
6744
  };
@@ -6575,6 +6757,7 @@ function claudeLimitsWindows(value) {
6575
6757
  windows.push({
6576
6758
  kind,
6577
6759
  ...typeof scope === "string" && scope.length > 0 ? { scope } : {},
6760
+ ...claudeWindowTiming(kind),
6578
6761
  usedPercent: entry.percent,
6579
6762
  ...resetsAt === void 0 ? {} : { resetsAt }
6580
6763
  });
@@ -7575,6 +7758,10 @@ function codexUsageWindow(value, fallbackKind) {
7575
7758
  else if (typeof window.reset_after_seconds === "number" && window.reset_after_seconds > 0) resetsAt = Date.now() + window.reset_after_seconds * 1e3;
7576
7759
  return {
7577
7760
  kind: codexWindowKind(window, fallbackKind),
7761
+ ...typeof window.limit_window_seconds === "number" && Number.isFinite(window.limit_window_seconds) && window.limit_window_seconds > 0 ? {
7762
+ windowDurationMs: window.limit_window_seconds * 1e3,
7763
+ fixedWindow: true
7764
+ } : {},
7578
7765
  usedPercent: window.used_percent,
7579
7766
  ...resetsAt === void 0 ? {} : { resetsAt }
7580
7767
  };
@@ -9106,13 +9293,19 @@ var PoolUsageTracker = class {
9106
9293
  const entry = this.entries.get(key);
9107
9294
  if (entry !== void 0 && Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) if (entry.snapshot !== void 0) {
9108
9295
  if (!force) return entry.snapshot;
9109
- } else if (entry.lastSnapshot !== void 0) return entry.lastSnapshot;
9296
+ } else if (entry.lastSnapshot !== void 0) return {
9297
+ ...entry.lastSnapshot,
9298
+ stale: true
9299
+ };
9110
9300
  else throw entry.error;
9111
9301
  try {
9112
9302
  return await this.refresh(key, fetcher);
9113
9303
  } catch (error) {
9114
9304
  const failed = this.entries.get(key);
9115
- if (failed?.snapshot === void 0 && failed?.lastSnapshot !== void 0) return failed.lastSnapshot;
9305
+ if (failed?.snapshot === void 0 && failed?.lastSnapshot !== void 0) return {
9306
+ ...failed.lastSnapshot,
9307
+ stale: true
9308
+ };
9116
9309
  throw error;
9117
9310
  }
9118
9311
  }
@@ -9137,10 +9330,16 @@ var PoolUsageTracker = class {
9137
9330
  if (pending === void 0) {
9138
9331
  const prior = this.entries.get(key);
9139
9332
  const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
9140
- pending = fetcher().then((snapshot) => {
9333
+ pending = fetcher().then((value) => {
9334
+ const at = Date.now();
9335
+ const snapshot = {
9336
+ ...value,
9337
+ observedAt: at,
9338
+ stale: false
9339
+ };
9141
9340
  this.entries.set(key, {
9142
9341
  snapshot,
9143
- at: Date.now()
9342
+ at
9144
9343
  });
9145
9344
  return snapshot;
9146
9345
  }, (error) => {
@@ -9445,10 +9644,12 @@ async function fetchGrokUsage(session, fetchFn = hostFetch, signal) {
9445
9644
  const kind = config.currentPeriod?.type === "USAGE_PERIOD_TYPE_WEEKLY" ? "weekly" : "other";
9446
9645
  const resetsAt = grokResetsAt(config.currentPeriod?.end);
9447
9646
  const usedPercent = typeof config.creditUsagePercent === "number" && Number.isFinite(config.creditUsagePercent) ? config.creditUsagePercent : 0;
9647
+ const startsAt = grokResetsAt(config.currentPeriod?.start);
9448
9648
  windows.push({
9449
9649
  kind,
9450
9650
  usedPercent,
9451
- ...resetsAt === void 0 ? {} : { resetsAt }
9651
+ ...resetsAt === void 0 ? {} : { resetsAt },
9652
+ ...startsAt === void 0 ? {} : { startsAt }
9452
9653
  });
9453
9654
  } else if (typeof config.monthlyLimit?.val === "number" && config.monthlyLimit.val > 0) {
9454
9655
  const used = typeof config.used?.val === "number" ? config.used.val : 0;
@@ -12999,16 +13200,29 @@ const Config = z.object({
12999
13200
  const DEFAULT_MODELS = {
13000
13201
  codex: [
13001
13202
  {
13002
- id: "gpt-5.1-codex",
13003
- name: "GPT-5.1 Codex"
13203
+ id: "gpt-6-astra",
13204
+ name: "GPT-6-Astra",
13205
+ contextWindow: 272e3
13206
+ },
13207
+ {
13208
+ id: "gpt-5.6-sol",
13209
+ name: "GPT-5.6-Sol",
13210
+ contextWindow: 272e3
13211
+ },
13212
+ {
13213
+ id: "gpt-5.6-terra",
13214
+ name: "GPT-5.6-Terra",
13215
+ contextWindow: 272e3
13004
13216
  },
13005
13217
  {
13006
- id: "gpt-5.1-codex-mini",
13007
- name: "GPT-5.1 Codex Mini"
13218
+ id: "gpt-5.6-luna",
13219
+ name: "GPT-5.6-Luna",
13220
+ contextWindow: 272e3
13008
13221
  },
13009
13222
  {
13010
- id: "gpt-5.1",
13011
- name: "GPT-5.1"
13223
+ id: "gpt-5.5",
13224
+ name: "GPT-5.5",
13225
+ contextWindow: 272e3
13012
13226
  }
13013
13227
  ],
13014
13228
  claude: [
@@ -13058,23 +13272,26 @@ const DEFAULT_MODELS = {
13058
13272
  ],
13059
13273
  copilot: [
13060
13274
  {
13061
- id: "gpt-4.1",
13062
- name: "GPT-4.1",
13063
- inputModalities: ["text", "image"]
13275
+ id: "gpt-6-sol",
13276
+ name: "GPT-6 Sol",
13277
+ inputModalities: ["text", "image"],
13278
+ wire: "responses"
13064
13279
  },
13065
13280
  {
13066
- id: "gpt-4o",
13067
- name: "GPT-4o",
13068
- inputModalities: ["text", "image"]
13281
+ id: "gpt-6-luna",
13282
+ name: "GPT-6 Luna",
13283
+ inputModalities: ["text", "image"],
13284
+ wire: "responses"
13069
13285
  },
13070
13286
  {
13071
- id: "claude-sonnet-4.5",
13072
- name: "Claude Sonnet 4.5",
13073
- inputModalities: ["text", "image"]
13287
+ id: "gpt-5.5",
13288
+ name: "GPT-5.5",
13289
+ inputModalities: ["text", "image"],
13290
+ wire: "responses"
13074
13291
  },
13075
13292
  {
13076
- id: "gemini-2.5-pro",
13077
- name: "Gemini 2.5 Pro",
13293
+ id: "claude-haiku-4.5",
13294
+ name: "Claude Haiku 4.5",
13078
13295
  inputModalities: ["text", "image"]
13079
13296
  }
13080
13297
  ],
@@ -256,6 +256,11 @@ function claudeResetsAt(value) {
256
256
  const parsed = Date.parse(value);
257
257
  return Number.isFinite(parsed) ? parsed : undefined;
258
258
  }
259
+ /** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
260
+ function claudeWindowTiming(kind) {
261
+ return kind === 'session' ? { windowDurationMs: 5 * 60 * 60_000, fixedWindow: true }
262
+ : kind === 'weekly' ? { windowDurationMs: 7 * 24 * 60 * 60_000, fixedWindow: true } : {};
263
+ }
259
264
  /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
260
265
  function claudeLegacyWindow(value, kind, scope) {
261
266
  if (typeof value !== 'object' || value === null)
@@ -267,6 +272,7 @@ function claudeLegacyWindow(value, kind, scope) {
267
272
  return {
268
273
  kind,
269
274
  ...scope === undefined ? {} : { scope },
275
+ ...claudeWindowTiming(kind),
270
276
  usedPercent: bucket.utilization,
271
277
  ...resetsAt === undefined ? {} : { resetsAt },
272
278
  };
@@ -290,6 +296,7 @@ function claudeLimitsWindows(value) {
290
296
  windows.push({
291
297
  kind,
292
298
  ...typeof scope === 'string' && scope.length > 0 ? { scope } : {},
299
+ ...claudeWindowTiming(kind),
293
300
  usedPercent: entry.percent,
294
301
  ...resetsAt === undefined ? {} : { resetsAt },
295
302
  });
@@ -256,6 +256,11 @@ function codexUsageWindow(value, fallbackKind) {
256
256
  }
257
257
  return {
258
258
  kind: codexWindowKind(window, fallbackKind),
259
+ ...typeof window.limit_window_seconds === 'number' && Number.isFinite(window.limit_window_seconds)
260
+ && window.limit_window_seconds > 0 ? {
261
+ // wham reports the duration and reset of this same quota window.
262
+ windowDurationMs: window.limit_window_seconds * 1000, fixedWindow: true,
263
+ } : {},
259
264
  usedPercent: window.used_percent,
260
265
  ...resetsAt === undefined ? {} : { resetsAt },
261
266
  };
@@ -193,9 +193,19 @@ export interface UsageWindow {
193
193
  usedPercent: number;
194
194
  /** Epoch milliseconds at which the window resets, when the provider discloses it. */
195
195
  resetsAt?: number;
196
+ /** Epoch milliseconds of the billing/window start, when disclosed. */
197
+ startsAt?: number;
198
+ /** Reported duration; not sufficient to establish fixed-window semantics. */
199
+ windowDurationMs?: number;
200
+ /** Provider-verified fixed interval; permits deriving start from end and duration. */
201
+ fixedWindow?: boolean;
196
202
  }
197
203
  /** Subscription usage of one provider, as served by the `usage` RPC endpoint. */
198
204
  export interface ProviderUsage {
205
+ /** Original successful observation time, preserved across cache reads. */
206
+ observedAt?: number;
207
+ /** Last refresh failed; cached numbers are display-only until recovery. */
208
+ stale?: boolean;
199
209
  /** False when the provider has no usage endpoint (grok); windows are absent then. */
200
210
  supported: boolean;
201
211
  /** Usage windows in display order. */
@@ -86,6 +86,10 @@ export async function fetchCursorUsage(accessToken, http = fetch, signal) {
86
86
  const resetValue = summary?.billingCycleEnd;
87
87
  const parsedReset = typeof resetValue === 'string' ? Date.parse(resetValue) : NaN;
88
88
  const resetsAt = Number.isFinite(parsedReset) ? parsedReset : undefined;
89
+ const startValue = summary?.billingCycleStart;
90
+ const parsedStart = typeof startValue === 'string' ? Date.parse(startValue) : NaN;
91
+ const timing = { ...(resetsAt === undefined ? {} : { resetsAt }),
92
+ ...(Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}) };
89
93
  const windows = [];
90
94
  for (const [field, scope] of [
91
95
  ['totalPercentUsed', 'Included'],
@@ -95,11 +99,11 @@ export async function fetchCursorUsage(accessToken, http = fetch, signal) {
95
99
  const usedPercent = number(plan?.[field]);
96
100
  if (usedPercent === undefined || usedPercent < 0 || usedPercent > 100)
97
101
  continue;
98
- windows.push({ kind: 'other', scope, usedPercent, ...(resetsAt === undefined ? {} : { resetsAt }) });
102
+ windows.push({ kind: 'other', scope, usedPercent, ...timing });
99
103
  }
100
104
  const legacy = legacyRequestWindow(legacyResult.status === 'fulfilled' ? legacyResult.value : undefined, resetsAt);
101
105
  if (legacy !== undefined)
102
- windows.push(legacy);
106
+ windows.push({ ...legacy, ...timing });
103
107
  if (windows.length === 0 && summary?.isUnlimited !== true) {
104
108
  throw new Error('Cursor usage response has no quota windows');
105
109
  }
@@ -1,6 +1,6 @@
1
1
  import type { ProviderUsage } from './common.js';
2
2
  /** Usage-only sources whose model routes are supplied by the DSH base install. */
3
- export type ExternalUsageSource = 'opencode-go' | 'kimi-code';
3
+ export type ExternalUsageSource = 'opencode-go' | 'kimi-code' | 'minimax' | 'minimax-cn';
4
4
  export declare const EXTERNAL_USAGE_SOURCES: readonly ExternalUsageSource[];
5
5
  export interface ExternalUsageStatus {
6
6
  configured: boolean;
@@ -1,8 +1,11 @@
1
+ import { fetchMiniMaxUsage } from './minimax-usage.js';
1
2
  import { fetchKimiCodeUsage, fetchOpenCodeGoUsage } from './external-usage.js';
2
- export const EXTERNAL_USAGE_SOURCES = ['opencode-go', 'kimi-code'];
3
+ export const EXTERNAL_USAGE_SOURCES = ['opencode-go', 'kimi-code', 'minimax', 'minimax-cn'];
3
4
  const DEFAULT_REFS = {
4
5
  'opencode-go': 'OPENCODE_GO_API_KEY',
5
6
  'kimi-code': 'KIMI_CODING_API_KEY',
7
+ 'minimax': 'MINIMAX_API_KEY',
8
+ 'minimax-cn': 'MINIMAX_CN_API_KEY',
6
9
  };
7
10
  export class ExternalUsageController {
8
11
  resolveCredential;
@@ -14,17 +17,19 @@ export class ExternalUsageController {
14
17
  this.refs = { ...DEFAULT_REFS, ...refs };
15
18
  }
16
19
  async status() {
17
- const [go, kimi] = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
20
+ const values = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
18
21
  const value = await this.resolveCredential(this.refs[source]);
19
22
  return typeof value?.value === 'string' && value.value.length > 0;
20
23
  }));
21
- return { 'opencode-go': { configured: go ?? false }, 'kimi-code': { configured: kimi ?? false } };
24
+ return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [source, { configured: values[i] ?? false }]));
22
25
  }
23
26
  async usage(source, signal) {
24
27
  const credential = await this.resolveCredential(this.refs[source]);
25
28
  if (credential?.value === undefined || credential.value.length === 0) {
26
29
  throw new Error(`${source} API key is not configured`);
27
30
  }
31
+ if (source === 'minimax' || source === 'minimax-cn')
32
+ return fetchMiniMaxUsage(credential.value, source === 'minimax-cn' ? 'cn' : 'global', this.http, signal);
28
33
  return source === 'opencode-go'
29
34
  ? fetchOpenCodeGoUsage(credential.value, this.http, signal)
30
35
  : fetchKimiCodeUsage(credential.value, this.http, signal);
@@ -3,6 +3,6 @@ import type { ProviderUsage } from './common.js';
3
3
  type HttpFetch = typeof fetch;
4
4
  /** Read the three account-wide OpenCode Go windows from its Go gateway. */
5
5
  export declare function fetchOpenCodeGoUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
6
- /** Read the Kimi Code plan windows using the same API as the Kimi Code CLI. */
6
+ /** Read official CLI count windows and retain compatibility with older ratio pools. */
7
7
  export declare function fetchKimiCodeUsage(apiKey: string, http?: HttpFetch, signal?: AbortSignal): Promise<ProviderUsage>;
8
8
  export {};