dsh-subscriptions 0.1.1 → 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.
- package/README.md +97 -4
- package/icon.svg +28 -0
- package/lib/client/CursorCard.js +15 -11
- package/lib/client/ExternalUsageCards.js +11 -11
- package/lib/client/SubscriptionUsageBadge.d.ts +6 -2
- package/lib/client/SubscriptionUsageBadge.js +56 -24
- package/lib/client/SubscriptionsSection.d.ts +7 -2
- package/lib/client/SubscriptionsSection.js +9 -13
- package/lib/client/UsageBadgeDisplaySetting.js +18 -1
- package/lib/client/UsageMeter.d.ts +8 -0
- package/lib/client/UsageMeter.js +51 -0
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +1 -0
- package/lib/client/locales.d.ts +48 -0
- package/lib/client/locales.js +48 -0
- package/lib/client/subscription-card-styles.js +0 -5
- package/lib/client/usage-color-preferences.d.ts +6 -0
- package/lib/client/usage-color-preferences.js +38 -0
- package/lib/client/usage-pace.d.ts +29 -0
- package/lib/client/usage-pace.js +58 -0
- package/lib/client.js +396 -114
- package/lib/client.js.map +1 -1
- package/lib/index.js +225 -35
- package/lib/providers/claude.js +7 -0
- package/lib/providers/codex.js +5 -0
- package/lib/providers/common.d.ts +10 -0
- package/lib/providers/cursor-usage.js +6 -2
- package/lib/providers/external-usage-controller.d.ts +1 -1
- package/lib/providers/external-usage-controller.js +8 -3
- package/lib/providers/external-usage.d.ts +1 -1
- package/lib/providers/external-usage.js +62 -2
- package/lib/providers/grok.js +3 -1
- package/lib/providers/minimax-usage.d.ts +3 -0
- package/lib/providers/minimax-usage.js +55 -0
- package/lib/providers/pool-usage.js +6 -4
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +7 -3
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
|
|
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
|
|
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 = [
|
|
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
|
|
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
|
-
...
|
|
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(
|
|
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
|
|
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
|
|
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((
|
|
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
|
|
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-
|
|
13003
|
-
name: "GPT-
|
|
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.
|
|
13007
|
-
name: "GPT-5.
|
|
13186
|
+
id: "gpt-5.6-terra",
|
|
13187
|
+
name: "GPT-5.6-Terra",
|
|
13188
|
+
contextWindow: 272e3
|
|
13008
13189
|
},
|
|
13009
13190
|
{
|
|
13010
|
-
id: "gpt-5.
|
|
13011
|
-
name: "GPT-5.
|
|
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-
|
|
13062
|
-
name: "GPT-
|
|
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-
|
|
13067
|
-
name: "GPT-
|
|
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: "
|
|
13072
|
-
name: "
|
|
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: "
|
|
13077
|
-
name: "
|
|
13266
|
+
id: "claude-haiku-4.5",
|
|
13267
|
+
name: "Claude Haiku 4.5",
|
|
13078
13268
|
inputModalities: ["text", "image"]
|
|
13079
13269
|
}
|
|
13080
13270
|
],
|
package/lib/providers/claude.js
CHANGED
|
@@ -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
|
});
|
package/lib/providers/codex.js
CHANGED
|
@@ -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, ...
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/lib/providers/grok.js
CHANGED
|
@@ -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
|
-
|
|
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;
|