dsh-subscriptions 0.1.0 → 0.1.2

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 (38) hide show
  1. package/README.md +99 -6
  2. package/icon.svg +28 -0
  3. package/lib/client/CursorCard.js +15 -11
  4. package/lib/client/ExternalUsageCards.js +11 -11
  5. package/lib/client/SubscriptionUsageBadge.d.ts +6 -2
  6. package/lib/client/SubscriptionUsageBadge.js +56 -24
  7. package/lib/client/SubscriptionsSection.d.ts +7 -2
  8. package/lib/client/SubscriptionsSection.js +9 -13
  9. package/lib/client/UsageBadgeDisplaySetting.js +18 -1
  10. package/lib/client/UsageMeter.d.ts +8 -0
  11. package/lib/client/UsageMeter.js +51 -0
  12. package/lib/client/host-contract.d.ts +1 -1
  13. package/lib/client/host-contract.js +1 -0
  14. package/lib/client/locales.d.ts +48 -0
  15. package/lib/client/locales.js +48 -0
  16. package/lib/client/subscription-card-styles.js +0 -5
  17. package/lib/client/usage-color-preferences.d.ts +6 -0
  18. package/lib/client/usage-color-preferences.js +38 -0
  19. package/lib/client/usage-pace.d.ts +29 -0
  20. package/lib/client/usage-pace.js +58 -0
  21. package/lib/client.js +396 -114
  22. package/lib/client.js.map +1 -1
  23. package/lib/index.js +225 -35
  24. package/lib/providers/claude.js +7 -0
  25. package/lib/providers/codex.js +5 -0
  26. package/lib/providers/common.d.ts +10 -0
  27. package/lib/providers/cursor-usage.js +6 -2
  28. package/lib/providers/external-usage-controller.d.ts +1 -1
  29. package/lib/providers/external-usage-controller.js +8 -3
  30. package/lib/providers/external-usage.d.ts +1 -1
  31. package/lib/providers/external-usage.js +62 -2
  32. package/lib/providers/grok.js +3 -1
  33. package/lib/providers/minimax-usage.d.ts +3 -0
  34. package/lib/providers/minimax-usage.js +55 -0
  35. package/lib/providers/pool-usage.js +6 -4
  36. package/locale/en.json +6 -0
  37. package/locale/zh.json +6 -0
  38. package/package.json +10 -6
package/lib/index.js CHANGED
@@ -1031,8 +1031,94 @@ async function setDefaultAccount(provider, account, path = authFilePath()) {
1031
1031
  });
1032
1032
  }
1033
1033
 
1034
+ //#endregion
1035
+ //#region src/providers/minimax-usage.ts
1036
+ /** MiniMax subscription quota, not the balance API for sk-api-* inference keys. */
1037
+ async function fetchMiniMaxUsage(key, region, http$1 = fetch, signal) {
1038
+ if (key.startsWith("sk-api-")) throw new Error("MiniMax requires a subscription key, not a pay-as-you-go key");
1039
+ const response = await http$1(`https://www.minimax.${region === "cn" ? "cn" : "io"}/v1/token_plan/remains`, {
1040
+ headers: {
1041
+ authorization: `Bearer ${key}`,
1042
+ "content-type": "application/json"
1043
+ },
1044
+ ...signal === void 0 ? {} : { signal }
1045
+ });
1046
+ if (!response.ok) throw new Error(`MiniMax usage endpoint returned HTTP ${response.status}`);
1047
+ const body = await response.json();
1048
+ 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");
1049
+ const windows = [];
1050
+ for (const raw of body.model_remains) {
1051
+ if (raw === null || typeof raw !== "object") continue;
1052
+ const row = raw;
1053
+ if (typeof row.model_name !== "string") continue;
1054
+ for (const [prefix, startKey, endKey, fallback] of [[
1055
+ "current_interval",
1056
+ "start_time",
1057
+ "end_time",
1058
+ "session"
1059
+ ], [
1060
+ "current_weekly",
1061
+ "weekly_start_time",
1062
+ "weekly_end_time",
1063
+ "weekly"
1064
+ ]]) {
1065
+ if (row[`${prefix}_status`] === 3) continue;
1066
+ const remaining = row[`${prefix}_remaining_percent`];
1067
+ const boost = prefix === "current_weekly" ? row.weekly_boost_permille ?? 1e3 : 1e3;
1068
+ if (typeof boost !== "number" || !Number.isFinite(boost) || boost <= 0) continue;
1069
+ const total = row[`${prefix}_total_count`];
1070
+ const count = row[`${prefix}_usage_count`];
1071
+ if (typeof total !== "number" || !Number.isFinite(total) || total <= 0) continue;
1072
+ const usedPercent = typeof remaining === "number" ? 100 - remaining * boost / 1e3 : typeof total === "number" && total > 0 && typeof count === "number" ? (1 - count / total) * 100 : void 0;
1073
+ if (usedPercent === void 0 || !Number.isFinite(usedPercent) || usedPercent < 0 || usedPercent > 100) continue;
1074
+ const start = row[startKey];
1075
+ const end = row[endKey];
1076
+ const validBounds = typeof start === "number" && Number.isFinite(start) && start > 0 && typeof end === "number" && Number.isFinite(end) && end > start;
1077
+ windows.push({
1078
+ kind: fallback === "session" && validBounds && end - start !== 18e6 ? "other" : fallback,
1079
+ scope: row.model_name,
1080
+ usedPercent,
1081
+ ...validBounds ? {
1082
+ startsAt: start,
1083
+ resetsAt: end
1084
+ } : {}
1085
+ });
1086
+ }
1087
+ }
1088
+ if (!windows.length) throw new Error("MiniMax has no supported finite quota windows");
1089
+ return {
1090
+ supported: true,
1091
+ plan: "MiniMax",
1092
+ windows
1093
+ };
1094
+ }
1095
+
1034
1096
  //#endregion
1035
1097
  //#region src/providers/external-usage.ts
1098
+ function numeric(value) {
1099
+ const n = typeof value === "string" && value.trim() !== "" ? Number(value) : value;
1100
+ return typeof n === "number" && Number.isFinite(n) ? n : void 0;
1101
+ }
1102
+ function kimiReset(value) {
1103
+ for (const key of [
1104
+ "reset_at",
1105
+ "resetAt",
1106
+ "reset_time",
1107
+ "resetTime"
1108
+ ]) {
1109
+ const reset = resetTime$1(value[key]);
1110
+ if (reset !== void 0) return reset;
1111
+ }
1112
+ for (const key of [
1113
+ "reset_in",
1114
+ "resetIn",
1115
+ "ttl",
1116
+ "window"
1117
+ ]) {
1118
+ const seconds = numeric(value[key]);
1119
+ if (seconds !== void 0 && seconds > 0) return Date.now() + seconds * 1e3;
1120
+ }
1121
+ }
1036
1122
  function record$3(value) {
1037
1123
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1038
1124
  }
@@ -1085,6 +1171,13 @@ async function fetchOpenCodeGoUsage(apiKey, http$1 = fetch, signal) {
1085
1171
  kind,
1086
1172
  ...scope === void 0 ? {} : { scope },
1087
1173
  usedPercent,
1174
+ ...field === "rolling" ? {
1175
+ fixedWindow: true,
1176
+ windowDurationMs: 18e6
1177
+ } : field === "weekly" ? {
1178
+ fixedWindow: true,
1179
+ windowDurationMs: 6048e5
1180
+ } : {},
1088
1181
  ...resetsAt === void 0 ? {} : { resetsAt }
1089
1182
  });
1090
1183
  }
@@ -1095,12 +1188,45 @@ async function fetchOpenCodeGoUsage(apiKey, http$1 = fetch, signal) {
1095
1188
  plan: "OpenCode Go"
1096
1189
  };
1097
1190
  }
1098
- /** Read the Kimi Code plan windows using the same API as the Kimi Code CLI. */
1191
+ /** Read official CLI count windows and retain compatibility with older ratio pools. */
1099
1192
  async function fetchKimiCodeUsage(apiKey, http$1 = fetch, signal) {
1100
1193
  const body = record$3(await getUsage("https://api.kimi.com/coding/v1/usages", apiKey, http$1, signal));
1101
1194
  if (body === void 0) throw new Error("Kimi Code usage response is not an object");
1195
+ const modern = [];
1196
+ const rows = [...record$3(body.usage) ? [{ detail: body.usage }] : [], ...Array.isArray(body.limits) ? body.limits : []];
1197
+ for (const raw of rows) {
1198
+ const item = record$3(raw);
1199
+ if (!item) continue;
1200
+ const detail = record$3(item.detail) ?? item;
1201
+ const limit = numeric(detail.limit);
1202
+ const used = numeric(detail.used) ?? (limit !== void 0 && numeric(detail.remaining) !== void 0 ? limit - numeric(detail.remaining) : void 0);
1203
+ if (limit === void 0 || limit <= 0 || used === void 0) continue;
1204
+ const usedPercent = percent(used / limit * 100);
1205
+ if (usedPercent === void 0) continue;
1206
+ const window = record$3(item.window) ?? item;
1207
+ const duration = numeric(window.duration ?? item.duration ?? detail.duration);
1208
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "").toUpperCase();
1209
+ const multiplier = unit.includes("MINUTE") ? 6e4 : unit.includes("HOUR") ? 36e5 : unit.includes("DAY") ? 864e5 : unit.includes("SECOND") ? 1e3 : void 0;
1210
+ const durationMs$1 = duration === void 0 || duration <= 0 || multiplier === void 0 ? void 0 : duration * multiplier;
1211
+ const kind = durationMs$1 === 18e6 ? "session" : durationMs$1 === 6048e5 ? "weekly" : "other";
1212
+ const resetsAt = kimiReset(detail);
1213
+ const scope = detail.name ?? detail.title ?? item.name ?? item.title;
1214
+ modern.push({
1215
+ kind,
1216
+ usedPercent,
1217
+ ...typeof scope === "string" ? { scope } : {},
1218
+ ...resetsAt === void 0 ? {} : { resetsAt },
1219
+ ...durationMs$1 === void 0 ? {} : { windowDurationMs: durationMs$1 },
1220
+ ...kind === "weekly" ? { fixedWindow: true } : {}
1221
+ });
1222
+ }
1223
+ if (modern.length) return {
1224
+ supported: true,
1225
+ windows: modern,
1226
+ plan: "Kimi Code"
1227
+ };
1102
1228
  const usages = record$3(body.usages);
1103
- if (usages === void 0) throw new Error("Kimi Code usage response has no usages object");
1229
+ if (usages === void 0) throw new Error("Kimi Code usage response has no valid windows");
1104
1230
  const windows = [];
1105
1231
  for (const [field, kind, scope] of [
1106
1232
  [
@@ -1127,6 +1253,10 @@ async function fetchKimiCodeUsage(apiKey, http$1 = fetch, signal) {
1127
1253
  kind,
1128
1254
  ...scope === void 0 ? {} : { scope },
1129
1255
  usedPercent: ratio * 100,
1256
+ ...field === "limit_7d" ? {
1257
+ fixedWindow: true,
1258
+ windowDurationMs: 6048e5
1259
+ } : {},
1130
1260
  ...resetsAt === void 0 ? {} : { resetsAt }
1131
1261
  });
1132
1262
  }
@@ -1139,10 +1269,17 @@ async function fetchKimiCodeUsage(apiKey, http$1 = fetch, signal) {
1139
1269
 
1140
1270
  //#endregion
1141
1271
  //#region src/providers/external-usage-controller.ts
1142
- const EXTERNAL_USAGE_SOURCES = ["opencode-go", "kimi-code"];
1272
+ const EXTERNAL_USAGE_SOURCES = [
1273
+ "opencode-go",
1274
+ "kimi-code",
1275
+ "minimax",
1276
+ "minimax-cn"
1277
+ ];
1143
1278
  const DEFAULT_REFS = {
1144
1279
  "opencode-go": "OPENCODE_GO_API_KEY",
1145
- "kimi-code": "KIMI_CODING_API_KEY"
1280
+ "kimi-code": "KIMI_CODING_API_KEY",
1281
+ "minimax": "MINIMAX_API_KEY",
1282
+ "minimax-cn": "MINIMAX_CN_API_KEY"
1146
1283
  };
1147
1284
  var ExternalUsageController = class {
1148
1285
  refs;
@@ -1155,18 +1292,16 @@ var ExternalUsageController = class {
1155
1292
  };
1156
1293
  }
1157
1294
  async status() {
1158
- const [go, kimi] = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
1295
+ const values = await Promise.all(EXTERNAL_USAGE_SOURCES.map(async (source) => {
1159
1296
  const value = await this.resolveCredential(this.refs[source]);
1160
1297
  return typeof value?.value === "string" && value.value.length > 0;
1161
1298
  }));
1162
- return {
1163
- "opencode-go": { configured: go ?? false },
1164
- "kimi-code": { configured: kimi ?? false }
1165
- };
1299
+ return Object.fromEntries(EXTERNAL_USAGE_SOURCES.map((source, i) => [source, { configured: values[i] ?? false }]));
1166
1300
  }
1167
1301
  async usage(source, signal) {
1168
1302
  const credential = await this.resolveCredential(this.refs[source]);
1169
1303
  if (credential?.value === void 0 || credential.value.length === 0) throw new Error(`${source} API key is not configured`);
1304
+ if (source === "minimax" || source === "minimax-cn") return fetchMiniMaxUsage(credential.value, source === "minimax-cn" ? "cn" : "global", this.http, signal);
1170
1305
  return source === "opencode-go" ? fetchOpenCodeGoUsage(credential.value, this.http, signal) : fetchKimiCodeUsage(credential.value, this.http, signal);
1171
1306
  }
1172
1307
  };
@@ -1766,6 +1901,12 @@ async function fetchCursorUsage(accessToken, http$1 = fetch, signal) {
1766
1901
  const resetValue = summary?.billingCycleEnd;
1767
1902
  const parsedReset = typeof resetValue === "string" ? Date.parse(resetValue) : NaN;
1768
1903
  const resetsAt = Number.isFinite(parsedReset) ? parsedReset : void 0;
1904
+ const startValue = summary?.billingCycleStart;
1905
+ const parsedStart = typeof startValue === "string" ? Date.parse(startValue) : NaN;
1906
+ const timing = {
1907
+ ...resetsAt === void 0 ? {} : { resetsAt },
1908
+ ...Number.isFinite(parsedStart) ? { startsAt: parsedStart } : {}
1909
+ };
1769
1910
  const windows = [];
1770
1911
  for (const [field, scope] of [
1771
1912
  ["totalPercentUsed", "Included"],
@@ -1778,11 +1919,14 @@ async function fetchCursorUsage(accessToken, http$1 = fetch, signal) {
1778
1919
  kind: "other",
1779
1920
  scope,
1780
1921
  usedPercent,
1781
- ...resetsAt === void 0 ? {} : { resetsAt }
1922
+ ...timing
1782
1923
  });
1783
1924
  }
1784
1925
  const legacy = legacyRequestWindow(legacyResult.status === "fulfilled" ? legacyResult.value : void 0, resetsAt);
1785
- if (legacy !== void 0) windows.push(legacy);
1926
+ if (legacy !== void 0) windows.push({
1927
+ ...legacy,
1928
+ ...timing
1929
+ });
1786
1930
  if (windows.length === 0 && summary?.isUnlimited !== true) throw new Error("Cursor usage response has no quota windows");
1787
1931
  const membershipType = summary?.membershipType;
1788
1932
  return {
@@ -4521,8 +4665,8 @@ function resetInstantFromValue(value, now) {
4521
4665
  if (typeof value !== "string") return void 0;
4522
4666
  const trimmed = value.trim();
4523
4667
  if (trimmed.length === 0) return void 0;
4524
- const numeric = Number(trimmed);
4525
- if (Number.isFinite(numeric)) return resetInstantFromNumber(numeric, now);
4668
+ const numeric$1 = Number(trimmed);
4669
+ if (Number.isFinite(numeric$1)) return resetInstantFromNumber(numeric$1, now);
4526
4670
  const duration = durationMs(trimmed);
4527
4671
  if (duration !== void 0) return now + duration;
4528
4672
  const parsed = Date.parse(trimmed);
@@ -6548,6 +6692,16 @@ function claudeResetsAt(value) {
6548
6692
  const parsed = Date.parse(value);
6549
6693
  return Number.isFinite(parsed) ? parsed : void 0;
6550
6694
  }
6695
+ /** Claude session and weekly buckets reset as whole five-hour/seven-day windows. */
6696
+ function claudeWindowTiming(kind) {
6697
+ return kind === "session" ? {
6698
+ windowDurationMs: 300 * 6e4,
6699
+ fixedWindow: true
6700
+ } : kind === "weekly" ? {
6701
+ windowDurationMs: 10080 * 6e4,
6702
+ fixedWindow: true
6703
+ } : {};
6704
+ }
6551
6705
  /** Map one legacy `{utilization, resets_at}` bucket; undefined when null or unusable. */
6552
6706
  function claudeLegacyWindow(value, kind, scope) {
6553
6707
  if (typeof value !== "object" || value === null) return void 0;
@@ -6557,6 +6711,7 @@ function claudeLegacyWindow(value, kind, scope) {
6557
6711
  return {
6558
6712
  kind,
6559
6713
  ...scope === void 0 ? {} : { scope },
6714
+ ...claudeWindowTiming(kind),
6560
6715
  usedPercent: bucket.utilization,
6561
6716
  ...resetsAt === void 0 ? {} : { resetsAt }
6562
6717
  };
@@ -6575,6 +6730,7 @@ function claudeLimitsWindows(value) {
6575
6730
  windows.push({
6576
6731
  kind,
6577
6732
  ...typeof scope === "string" && scope.length > 0 ? { scope } : {},
6733
+ ...claudeWindowTiming(kind),
6578
6734
  usedPercent: entry.percent,
6579
6735
  ...resetsAt === void 0 ? {} : { resetsAt }
6580
6736
  });
@@ -7575,6 +7731,10 @@ function codexUsageWindow(value, fallbackKind) {
7575
7731
  else if (typeof window.reset_after_seconds === "number" && window.reset_after_seconds > 0) resetsAt = Date.now() + window.reset_after_seconds * 1e3;
7576
7732
  return {
7577
7733
  kind: codexWindowKind(window, fallbackKind),
7734
+ ...typeof window.limit_window_seconds === "number" && Number.isFinite(window.limit_window_seconds) && window.limit_window_seconds > 0 ? {
7735
+ windowDurationMs: window.limit_window_seconds * 1e3,
7736
+ fixedWindow: true
7737
+ } : {},
7578
7738
  usedPercent: window.used_percent,
7579
7739
  ...resetsAt === void 0 ? {} : { resetsAt }
7580
7740
  };
@@ -9106,13 +9266,19 @@ var PoolUsageTracker = class {
9106
9266
  const entry = this.entries.get(key);
9107
9267
  if (entry !== void 0 && Date.now() - entry.at < (entry.cooldownMs ?? this.ttlMs)) if (entry.snapshot !== void 0) {
9108
9268
  if (!force) return entry.snapshot;
9109
- } else if (entry.lastSnapshot !== void 0) return entry.lastSnapshot;
9269
+ } else if (entry.lastSnapshot !== void 0) return {
9270
+ ...entry.lastSnapshot,
9271
+ stale: true
9272
+ };
9110
9273
  else throw entry.error;
9111
9274
  try {
9112
9275
  return await this.refresh(key, fetcher);
9113
9276
  } catch (error) {
9114
9277
  const failed = this.entries.get(key);
9115
- if (failed?.snapshot === void 0 && failed?.lastSnapshot !== void 0) return failed.lastSnapshot;
9278
+ if (failed?.snapshot === void 0 && failed?.lastSnapshot !== void 0) return {
9279
+ ...failed.lastSnapshot,
9280
+ stale: true
9281
+ };
9116
9282
  throw error;
9117
9283
  }
9118
9284
  }
@@ -9137,10 +9303,16 @@ var PoolUsageTracker = class {
9137
9303
  if (pending === void 0) {
9138
9304
  const prior = this.entries.get(key);
9139
9305
  const lastSnapshot = prior?.snapshot ?? prior?.lastSnapshot;
9140
- pending = fetcher().then((snapshot) => {
9306
+ pending = fetcher().then((value) => {
9307
+ const at = Date.now();
9308
+ const snapshot = {
9309
+ ...value,
9310
+ observedAt: at,
9311
+ stale: false
9312
+ };
9141
9313
  this.entries.set(key, {
9142
9314
  snapshot,
9143
- at: Date.now()
9315
+ at
9144
9316
  });
9145
9317
  return snapshot;
9146
9318
  }, (error) => {
@@ -9445,10 +9617,12 @@ async function fetchGrokUsage(session, fetchFn = hostFetch, signal) {
9445
9617
  const kind = config.currentPeriod?.type === "USAGE_PERIOD_TYPE_WEEKLY" ? "weekly" : "other";
9446
9618
  const resetsAt = grokResetsAt(config.currentPeriod?.end);
9447
9619
  const usedPercent = typeof config.creditUsagePercent === "number" && Number.isFinite(config.creditUsagePercent) ? config.creditUsagePercent : 0;
9620
+ const startsAt = grokResetsAt(config.currentPeriod?.start);
9448
9621
  windows.push({
9449
9622
  kind,
9450
9623
  usedPercent,
9451
- ...resetsAt === void 0 ? {} : { resetsAt }
9624
+ ...resetsAt === void 0 ? {} : { resetsAt },
9625
+ ...startsAt === void 0 ? {} : { startsAt }
9452
9626
  });
9453
9627
  } else if (typeof config.monthlyLimit?.val === "number" && config.monthlyLimit.val > 0) {
9454
9628
  const used = typeof config.used?.val === "number" ? config.used.val : 0;
@@ -12999,16 +13173,29 @@ const Config = z.object({
12999
13173
  const DEFAULT_MODELS = {
13000
13174
  codex: [
13001
13175
  {
13002
- id: "gpt-5.1-codex",
13003
- name: "GPT-5.1 Codex"
13176
+ id: "gpt-6-astra",
13177
+ name: "GPT-6-Astra",
13178
+ contextWindow: 272e3
13179
+ },
13180
+ {
13181
+ id: "gpt-5.6-sol",
13182
+ name: "GPT-5.6-Sol",
13183
+ contextWindow: 272e3
13004
13184
  },
13005
13185
  {
13006
- id: "gpt-5.1-codex-mini",
13007
- name: "GPT-5.1 Codex Mini"
13186
+ id: "gpt-5.6-terra",
13187
+ name: "GPT-5.6-Terra",
13188
+ contextWindow: 272e3
13008
13189
  },
13009
13190
  {
13010
- id: "gpt-5.1",
13011
- name: "GPT-5.1"
13191
+ id: "gpt-5.6-luna",
13192
+ name: "GPT-5.6-Luna",
13193
+ contextWindow: 272e3
13194
+ },
13195
+ {
13196
+ id: "gpt-5.5",
13197
+ name: "GPT-5.5",
13198
+ contextWindow: 272e3
13012
13199
  }
13013
13200
  ],
13014
13201
  claude: [
@@ -13058,23 +13245,26 @@ const DEFAULT_MODELS = {
13058
13245
  ],
13059
13246
  copilot: [
13060
13247
  {
13061
- id: "gpt-4.1",
13062
- name: "GPT-4.1",
13063
- inputModalities: ["text", "image"]
13248
+ id: "gpt-6-sol",
13249
+ name: "GPT-6 Sol",
13250
+ inputModalities: ["text", "image"],
13251
+ wire: "responses"
13064
13252
  },
13065
13253
  {
13066
- id: "gpt-4o",
13067
- name: "GPT-4o",
13068
- inputModalities: ["text", "image"]
13254
+ id: "gpt-6-luna",
13255
+ name: "GPT-6 Luna",
13256
+ inputModalities: ["text", "image"],
13257
+ wire: "responses"
13069
13258
  },
13070
13259
  {
13071
- id: "claude-sonnet-4.5",
13072
- name: "Claude Sonnet 4.5",
13073
- inputModalities: ["text", "image"]
13260
+ id: "gpt-5.5",
13261
+ name: "GPT-5.5",
13262
+ inputModalities: ["text", "image"],
13263
+ wire: "responses"
13074
13264
  },
13075
13265
  {
13076
- id: "gemini-2.5-pro",
13077
- name: "Gemini 2.5 Pro",
13266
+ id: "claude-haiku-4.5",
13267
+ name: "Claude Haiku 4.5",
13078
13268
  inputModalities: ["text", "image"]
13079
13269
  }
13080
13270
  ],
@@ -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 {};
@@ -1,4 +1,21 @@
1
1
  /** Usage readers for subscriptions whose model adapters are built into DSH. */
2
+ function numeric(value) {
3
+ const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
4
+ return typeof n === 'number' && Number.isFinite(n) ? n : undefined;
5
+ }
6
+ function kimiReset(value) {
7
+ for (const key of ['reset_at', 'resetAt', 'reset_time', 'resetTime']) {
8
+ const reset = resetTime(value[key]);
9
+ if (reset !== undefined)
10
+ return reset;
11
+ }
12
+ for (const key of ['reset_in', 'resetIn', 'ttl', 'window']) {
13
+ const seconds = numeric(value[key]);
14
+ if (seconds !== undefined && seconds > 0)
15
+ return Date.now() + seconds * 1000;
16
+ }
17
+ return undefined;
18
+ }
2
19
  function record(value) {
3
20
  return value !== null && typeof value === 'object' && !Array.isArray(value)
4
21
  ? value
@@ -45,6 +62,9 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
45
62
  kind,
46
63
  ...(scope === undefined ? {} : { scope }),
47
64
  usedPercent,
65
+ // Go's rolling counter is first-use anchored, not per-request sliding.
66
+ ...(field === 'rolling' ? { fixedWindow: true, windowDurationMs: 18_000_000 }
67
+ : field === 'weekly' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
48
68
  ...(resetsAt === undefined ? {} : { resetsAt }),
49
69
  });
50
70
  }
@@ -52,14 +72,53 @@ export async function fetchOpenCodeGoUsage(apiKey, http = fetch, signal) {
52
72
  throw new Error('OpenCode Go usage response has no valid windows');
53
73
  return { supported: true, windows, plan: 'OpenCode Go' };
54
74
  }
55
- /** Read the Kimi Code plan windows using the same API as the Kimi Code CLI. */
75
+ /** Read official CLI count windows and retain compatibility with older ratio pools. */
56
76
  export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
57
77
  const body = record(await getUsage('https://api.kimi.com/coding/v1/usages', apiKey, http, signal));
58
78
  if (body === undefined)
59
79
  throw new Error('Kimi Code usage response is not an object');
80
+ const modern = [];
81
+ const rows = [
82
+ ...(record(body.usage) ? [{ detail: body.usage }] : []),
83
+ ...(Array.isArray(body.limits) ? body.limits : []),
84
+ ];
85
+ for (const raw of rows) {
86
+ const item = record(raw);
87
+ if (!item)
88
+ continue;
89
+ const detail = record(item.detail) ?? item;
90
+ const limit = numeric(detail.limit);
91
+ const used = numeric(detail.used) ?? (limit !== undefined && numeric(detail.remaining) !== undefined
92
+ ? limit - numeric(detail.remaining) : undefined);
93
+ if (limit === undefined || limit <= 0 || used === undefined)
94
+ continue;
95
+ const usedPercent = percent(used / limit * 100);
96
+ if (usedPercent === undefined)
97
+ continue;
98
+ const window = record(item.window) ?? item;
99
+ const duration = numeric(window.duration ?? item.duration ?? detail.duration);
100
+ const unit = String(window.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? '').toUpperCase();
101
+ const multiplier = unit.includes('MINUTE') ? 60_000 : unit.includes('HOUR') ? 3_600_000
102
+ : unit.includes('DAY') ? 86_400_000 : unit.includes('SECOND') ? 1000 : undefined;
103
+ const durationMs = duration === undefined || duration <= 0 || multiplier === undefined
104
+ ? undefined : duration * multiplier;
105
+ const kind = durationMs === 18_000_000 ? 'session' : durationMs === 604_800_000 ? 'weekly' : 'other';
106
+ const resetsAt = kimiReset(detail);
107
+ const scope = detail.name ?? detail.title ?? item.name ?? item.title;
108
+ modern.push({ kind, usedPercent,
109
+ ...(typeof scope === 'string' ? { scope } : {}),
110
+ ...(resetsAt === undefined ? {} : { resetsAt }),
111
+ ...(durationMs === undefined ? {} : { windowDurationMs: durationMs }),
112
+ // Legacy weekly plans reset as whole seven-day buckets; five-hour semantics
113
+ // are documented as rolling, so no fixed-window assertion is made there.
114
+ ...(kind === 'weekly' ? { fixedWindow: true } : {}),
115
+ });
116
+ }
117
+ if (modern.length)
118
+ return { supported: true, windows: modern, plan: 'Kimi Code' };
60
119
  const usages = record(body.usages);
61
120
  if (usages === undefined)
62
- throw new Error('Kimi Code usage response has no usages object');
121
+ throw new Error('Kimi Code usage response has no valid windows');
63
122
  const windows = [];
64
123
  for (const [field, kind, scope] of [
65
124
  ['limit_5h', 'session', undefined],
@@ -75,6 +134,7 @@ export async function fetchKimiCodeUsage(apiKey, http = fetch, signal) {
75
134
  kind,
76
135
  ...(scope === undefined ? {} : { scope }),
77
136
  usedPercent: ratio * 100,
137
+ ...(field === 'limit_7d' ? { fixedWindow: true, windowDurationMs: 604_800_000 } : {}),
78
138
  ...(resetsAt === undefined ? {} : { resetsAt }),
79
139
  });
80
140
  }
@@ -272,7 +272,9 @@ export async function fetchGrokUsage(session, fetchFn = hostFetch, signal) {
272
272
  const usedPercent = typeof config.creditUsagePercent === 'number' && Number.isFinite(config.creditUsagePercent)
273
273
  ? config.creditUsagePercent
274
274
  : 0;
275
- windows.push({ kind, usedPercent, ...resetsAt === undefined ? {} : { resetsAt } });
275
+ const startsAt = grokResetsAt(config.currentPeriod?.start);
276
+ windows.push({ kind, usedPercent, ...resetsAt === undefined ? {} : { resetsAt },
277
+ ...startsAt === undefined ? {} : { startsAt } });
276
278
  }
277
279
  else if (typeof config.monthlyLimit?.val === 'number' && config.monthlyLimit.val > 0) {
278
280
  const used = typeof config.used?.val === 'number' ? config.used.val : 0;