@leo000001/opencode-quota-sidebar 3.0.10 → 4.0.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 (53) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +163 -42
  3. package/README.zh-CN.md +163 -42
  4. package/SECURITY.md +1 -1
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +354 -0
  7. package/dist/cli_render.d.ts +17 -0
  8. package/dist/cli_render.js +292 -0
  9. package/dist/events.d.ts +1 -1
  10. package/dist/events.js +2 -2
  11. package/dist/format.d.ts +4 -0
  12. package/dist/format.js +391 -49
  13. package/dist/history_messages.d.ts +8 -0
  14. package/dist/history_messages.js +157 -0
  15. package/dist/history_usage.d.ts +93 -0
  16. package/dist/history_usage.js +251 -0
  17. package/dist/index.js +29 -4
  18. package/dist/period.d.ts +29 -1
  19. package/dist/period.js +187 -9
  20. package/dist/provider_catalog.d.ts +8 -0
  21. package/dist/provider_catalog.js +68 -0
  22. package/dist/providers/core/anthropic.d.ts +1 -1
  23. package/dist/providers/core/anthropic.js +69 -45
  24. package/dist/providers/core/openai.js +38 -2
  25. package/dist/providers/index.d.ts +1 -2
  26. package/dist/providers/index.js +1 -3
  27. package/dist/quota.d.ts +4 -2
  28. package/dist/quota.js +18 -21
  29. package/dist/quota_render.d.ts +1 -1
  30. package/dist/quota_render.js +23 -24
  31. package/dist/quota_service.d.ts +1 -0
  32. package/dist/quota_service.js +151 -19
  33. package/dist/storage.d.ts +1 -1
  34. package/dist/storage.js +4 -4
  35. package/dist/storage_dates.d.ts +1 -1
  36. package/dist/storage_dates.js +8 -5
  37. package/dist/storage_parse.js +23 -1
  38. package/dist/supported_quota.d.ts +4 -0
  39. package/dist/supported_quota.js +36 -0
  40. package/dist/title.js +21 -10
  41. package/dist/tools.d.ts +14 -3
  42. package/dist/tools.js +54 -2
  43. package/dist/tui.tsx +17 -6
  44. package/dist/tui_helpers.js +11 -6
  45. package/dist/types.d.ts +8 -0
  46. package/dist/usage.d.ts +18 -0
  47. package/dist/usage.js +93 -9
  48. package/dist/usage_service.d.ts +4 -1
  49. package/dist/usage_service.js +193 -189
  50. package/package.json +4 -1
  51. package/quota-sidebar.config.example.json +36 -45
  52. package/dist/providers/third_party/xyai.d.ts +0 -2
  53. package/dist/providers/third_party/xyai.js +0 -348
package/dist/period.js CHANGED
@@ -1,14 +1,192 @@
1
- export function periodStart(period) {
2
- const now = new Date();
1
+ const MONTH_NAMES = [
2
+ 'Jan',
3
+ 'Feb',
4
+ 'Mar',
5
+ 'Apr',
6
+ 'May',
7
+ 'Jun',
8
+ 'Jul',
9
+ 'Aug',
10
+ 'Sep',
11
+ 'Oct',
12
+ 'Nov',
13
+ 'Dec',
14
+ ];
15
+ function pad2(value) {
16
+ return `${value}`.padStart(2, '0');
17
+ }
18
+ function startOfDay(timestamp) {
19
+ const date = new Date(timestamp);
20
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
21
+ }
22
+ function startOfMonth(timestamp) {
23
+ const date = new Date(timestamp);
24
+ return new Date(date.getFullYear(), date.getMonth(), 1).getTime();
25
+ }
26
+ function startOfWeek(timestamp) {
27
+ const date = new Date(timestamp);
28
+ const day = date.getDay();
29
+ const shift = day === 0 ? 6 : day - 1;
30
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() - shift).getTime();
31
+ }
32
+ function nextDayStart(timestamp) {
33
+ const date = new Date(timestamp);
34
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1).getTime();
35
+ }
36
+ function nextWeekStart(timestamp) {
37
+ const weekStart = new Date(startOfWeek(timestamp));
38
+ return new Date(weekStart.getFullYear(), weekStart.getMonth(), weekStart.getDate() + 7).getTime();
39
+ }
40
+ function nextMonthStart(timestamp) {
41
+ const date = new Date(timestamp);
42
+ return new Date(date.getFullYear(), date.getMonth() + 1, 1).getTime();
43
+ }
44
+ function formatLocalDate(timestamp) {
45
+ const date = new Date(timestamp);
46
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
47
+ }
48
+ function formatMonthInput(timestamp) {
49
+ const date = new Date(timestamp);
50
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}`;
51
+ }
52
+ function formatMonthLabel(timestamp) {
53
+ const date = new Date(timestamp);
54
+ return `${MONTH_NAMES[date.getMonth()]} ${date.getFullYear()}`;
55
+ }
56
+ function formatMonthShortLabel(timestamp) {
57
+ const date = new Date(timestamp);
58
+ return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}`;
59
+ }
60
+ function formatDayShortLabel(timestamp) {
61
+ const date = new Date(timestamp);
62
+ return `${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
63
+ }
64
+ function periodBoundaryStart(period, timestamp) {
65
+ if (period === 'month')
66
+ return startOfMonth(timestamp);
67
+ if (period === 'week')
68
+ return startOfWeek(timestamp);
69
+ return startOfDay(timestamp);
70
+ }
71
+ function nextPeriodBoundary(period, timestamp) {
72
+ if (period === 'month')
73
+ return nextMonthStart(timestamp);
74
+ if (period === 'week')
75
+ return nextWeekStart(timestamp);
76
+ return nextDayStart(timestamp);
77
+ }
78
+ function periodRangeLabels(period, startAt, endAt) {
3
79
  if (period === 'month') {
4
- return new Date(now.getFullYear(), now.getMonth(), 1).getTime();
80
+ return {
81
+ label: formatMonthLabel(startAt),
82
+ shortLabel: formatMonthShortLabel(startAt),
83
+ };
84
+ }
85
+ if (period === 'week') {
86
+ const endLabel = formatLocalDate(Math.max(startAt, endAt - 1));
87
+ const startLabel = formatLocalDate(startAt);
88
+ return {
89
+ label: `${startLabel} to ${endLabel}`,
90
+ shortLabel: `${startLabel}..${endLabel}`,
91
+ };
92
+ }
93
+ return {
94
+ label: formatLocalDate(startAt),
95
+ shortLabel: formatDayShortLabel(startAt),
96
+ };
97
+ }
98
+ export function parseSince(raw, now = Date.now()) {
99
+ const value = raw.trim();
100
+ const monthMatch = /^(\d{4})-(\d{2})$/.exec(value);
101
+ if (monthMatch) {
102
+ const year = Number(monthMatch[1]);
103
+ const month = Number(monthMatch[2]);
104
+ if (year < 100 || month < 1 || month > 12) {
105
+ throw new Error('`since` is not a valid calendar date');
106
+ }
107
+ const startAt = new Date(year, month - 1, 1).getTime();
108
+ if (Number.isNaN(startAt)) {
109
+ throw new Error('`since` is not a valid calendar date');
110
+ }
111
+ if (startAt > now) {
112
+ throw new Error('`since` cannot be in the future');
113
+ }
114
+ return { raw: value, precision: 'month', startAt };
115
+ }
116
+ const dayMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
117
+ if (dayMatch) {
118
+ const year = Number(dayMatch[1]);
119
+ const month = Number(dayMatch[2]);
120
+ const day = Number(dayMatch[3]);
121
+ if (year < 100) {
122
+ throw new Error('`since` is not a valid calendar date');
123
+ }
124
+ const startAt = new Date(year, month - 1, day).getTime();
125
+ const probe = new Date(startAt);
126
+ if (Number.isNaN(startAt) ||
127
+ probe.getFullYear() !== year ||
128
+ probe.getMonth() !== month - 1 ||
129
+ probe.getDate() !== day) {
130
+ throw new Error('`since` is not a valid calendar date');
131
+ }
132
+ if (startAt > now) {
133
+ throw new Error('`since` cannot be in the future');
134
+ }
135
+ return { raw: value, precision: 'day', startAt };
136
+ }
137
+ throw new Error('`since` must be `YYYY-MM` or `YYYY-MM-DD`');
138
+ }
139
+ export function periodRanges(period, since, endAt = Date.now()) {
140
+ if (since.startAt > endAt) {
141
+ throw new Error('`since` cannot be in the future');
142
+ }
143
+ const ranges = [];
144
+ let cursor = since.startAt;
145
+ let index = 0;
146
+ while (cursor < endAt) {
147
+ const boundaryStart = periodBoundaryStart(period, cursor);
148
+ const boundaryEnd = nextPeriodBoundary(period, cursor);
149
+ const rangeEnd = Math.min(boundaryEnd, endAt);
150
+ const isCurrent = rangeEnd === endAt && endAt !== periodBoundaryStart(period, endAt);
151
+ const isPartial = cursor !== boundaryStart || isCurrent;
152
+ const { label, shortLabel } = periodRangeLabels(period, cursor, rangeEnd);
153
+ ranges.push({
154
+ period,
155
+ startAt: cursor,
156
+ endAt: rangeEnd,
157
+ label,
158
+ shortLabel,
159
+ isCurrent,
160
+ isPartial,
161
+ index,
162
+ });
163
+ cursor = rangeEnd;
164
+ index += 1;
165
+ }
166
+ if (period === 'day' && ranges.length > 90) {
167
+ throw new Error('day history is limited to 90 days; choose a later `since` date');
168
+ }
169
+ return ranges;
170
+ }
171
+ export function periodStart(period, now = Date.now()) {
172
+ return periodBoundaryStart(period, now);
173
+ }
174
+ export function sinceFromLast(period, last, now = Date.now()) {
175
+ if (!Number.isInteger(last) || last < 1) {
176
+ throw new Error('`last` must be a positive integer');
177
+ }
178
+ const currentStart = periodBoundaryStart(period, now);
179
+ if (period === 'day') {
180
+ const date = new Date(currentStart);
181
+ const start = new Date(date.getFullYear(), date.getMonth(), date.getDate() - (last - 1)).getTime();
182
+ return formatLocalDate(start);
5
183
  }
6
184
  if (period === 'week') {
7
- const day = now.getDay();
8
- const shift = day === 0 ? 6 : day - 1;
9
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - shift);
10
- start.setHours(0, 0, 0, 0);
11
- return start.getTime();
185
+ const date = new Date(currentStart);
186
+ const start = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 7 * (last - 1)).getTime();
187
+ return formatLocalDate(start);
12
188
  }
13
- return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
189
+ const date = new Date(currentStart);
190
+ const start = new Date(date.getFullYear(), date.getMonth() - (last - 1), 1).getTime();
191
+ return formatMonthInput(start);
14
192
  }
@@ -0,0 +1,8 @@
1
+ import type { HistoryUsageResult } from './usage_service.js';
2
+ import type { UsageSummary } from './usage.js';
3
+ export declare function listCurrentProviderIDs(input: {
4
+ client: unknown;
5
+ directory: string;
6
+ }): Promise<Set<string>>;
7
+ export declare function filterUsageProvidersForDisplay(usage: UsageSummary, allowedProviderIDs: ReadonlySet<string>): UsageSummary;
8
+ export declare function filterHistoryProvidersForDisplay(result: HistoryUsageResult, allowedProviderIDs: ReadonlySet<string>): HistoryUsageResult;
@@ -0,0 +1,68 @@
1
+ function isRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ function providerListFromResponse(response) {
5
+ const data = isRecord(response) && Object.prototype.hasOwnProperty.call(response, 'data')
6
+ ? response.data
7
+ : undefined;
8
+ const record = isRecord(data) ? data : undefined;
9
+ return Array.isArray(record?.providers)
10
+ ? record.providers
11
+ : Array.isArray(record?.all)
12
+ ? record.all
13
+ : Array.isArray(data)
14
+ ? data
15
+ : undefined;
16
+ }
17
+ export async function listCurrentProviderIDs(input) {
18
+ const client = input.client;
19
+ const ids = new Set();
20
+ const collect = (response) => {
21
+ const list = providerListFromResponse(response);
22
+ if (!list)
23
+ return false;
24
+ for (const item of list) {
25
+ if (!isRecord(item))
26
+ continue;
27
+ if (typeof item.id === 'string' && item.id)
28
+ ids.add(item.id);
29
+ }
30
+ return ids.size > 0;
31
+ };
32
+ if (client.config?.providers) {
33
+ const response = await client.config.providers({
34
+ query: { directory: input.directory },
35
+ throwOnError: true,
36
+ });
37
+ if (collect(response))
38
+ return ids;
39
+ }
40
+ if (client.provider?.list) {
41
+ const response = await client.provider.list({
42
+ query: { directory: input.directory },
43
+ throwOnError: true,
44
+ });
45
+ collect(response);
46
+ }
47
+ return ids;
48
+ }
49
+ export function filterUsageProvidersForDisplay(usage, allowedProviderIDs) {
50
+ if (allowedProviderIDs.size === 0)
51
+ return usage;
52
+ return {
53
+ ...usage,
54
+ providers: Object.fromEntries(Object.entries(usage.providers).filter(([providerID]) => allowedProviderIDs.has(providerID))),
55
+ };
56
+ }
57
+ export function filterHistoryProvidersForDisplay(result, allowedProviderIDs) {
58
+ if (allowedProviderIDs.size === 0)
59
+ return result;
60
+ return {
61
+ ...result,
62
+ rows: result.rows.map((row) => ({
63
+ ...row,
64
+ usage: filterUsageProvidersForDisplay(row.usage, allowedProviderIDs),
65
+ })),
66
+ total: filterUsageProvidersForDisplay(result.total, allowedProviderIDs),
67
+ };
68
+ }
@@ -1,2 +1,2 @@
1
- import type { QuotaProviderAdapter } from "../types.js";
1
+ import type { QuotaProviderAdapter } from '../types.js';
2
2
  export declare const anthropicAdapter: QuotaProviderAdapter;
@@ -1,13 +1,12 @@
1
- import { swallow } from "../../helpers.js";
2
- import { asRecord, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from "../common.js";
3
- const ANTHROPIC_OAUTH_USAGE_BETA = "oauth-2025-04-20";
1
+ import { asRecord, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from '../common.js';
2
+ const ANTHROPIC_OAUTH_USAGE_BETA = 'oauth-2025-04-20';
4
3
  const ANTHROPIC_WINDOW_FIELDS = [
5
- ["five_hour", "5h"],
6
- ["seven_day", "Weekly"],
7
- ["seven_day_sonnet", "Sonnet 7d"],
8
- ["seven_day_opus", "Opus 7d"],
9
- ["seven_day_oauth_apps", "OAuth Apps 7d"],
10
- ["seven_day_cowork", "Cowork 7d"],
4
+ ['five_hour', '5h'],
5
+ ['seven_day', 'Weekly'],
6
+ ['seven_day_sonnet', 'Sonnet 7d'],
7
+ ['seven_day_opus', 'Opus 7d'],
8
+ ['seven_day_oauth_apps', 'OAuth Apps 7d'],
9
+ ['seven_day_cowork', 'Cowork 7d'],
11
10
  ];
12
11
  function parseAnthropicWindow(value, label) {
13
12
  const win = asRecord(value);
@@ -24,96 +23,121 @@ function parseAnthropicWindow(value, label) {
24
23
  };
25
24
  return parsed;
26
25
  }
26
+ function anthropicFetchErrorNote(error) {
27
+ if (error instanceof Error && error.name === 'AbortError') {
28
+ return 'timeout';
29
+ }
30
+ return 'network request failed';
31
+ }
32
+ function isRetryableAnthropicStatus(status) {
33
+ return status === 408 || status === 429 || status >= 500;
34
+ }
35
+ async function fetchAnthropicUsage(accessToken, timeoutMs) {
36
+ let lastErrorNote;
37
+ for (let attempt = 0; attempt < 2; attempt++) {
38
+ try {
39
+ const response = await fetchWithTimeout('https://api.anthropic.com/api/oauth/usage', {
40
+ method: 'GET',
41
+ headers: {
42
+ Accept: 'application/json',
43
+ Authorization: `Bearer ${accessToken}`,
44
+ 'Content-Type': 'application/json',
45
+ 'User-Agent': 'opencode-quota-sidebar',
46
+ 'anthropic-beta': ANTHROPIC_OAUTH_USAGE_BETA,
47
+ },
48
+ }, timeoutMs);
49
+ if (response.ok ||
50
+ !isRetryableAnthropicStatus(response.status) ||
51
+ attempt > 0) {
52
+ return { response };
53
+ }
54
+ lastErrorNote = `http ${response.status}`;
55
+ }
56
+ catch (error) {
57
+ lastErrorNote = anthropicFetchErrorNote(error);
58
+ }
59
+ }
60
+ return { errorNote: lastErrorNote || 'network request failed' };
61
+ }
27
62
  async function fetchAnthropicQuota({ providerID, auth, config, }) {
28
63
  const checkedAt = Date.now();
29
64
  const base = {
30
65
  providerID,
31
- adapterID: "anthropic",
32
- label: "Anthropic",
33
- shortLabel: "Anthropic",
66
+ adapterID: 'anthropic',
67
+ label: 'Anthropic',
68
+ shortLabel: 'Anthropic',
34
69
  sortOrder: 30,
35
70
  };
36
71
  if (!auth) {
37
72
  return {
38
73
  ...base,
39
- status: "unavailable",
74
+ status: 'unavailable',
40
75
  checkedAt,
41
- note: "auth not found",
76
+ note: 'auth not found',
42
77
  };
43
78
  }
44
- if (auth.type !== "oauth") {
79
+ if (auth.type !== 'oauth') {
45
80
  return {
46
81
  ...base,
47
- status: "unsupported",
82
+ status: 'unsupported',
48
83
  checkedAt,
49
- note: "api key auth has no quota endpoint",
84
+ note: 'api key auth has no quota endpoint',
50
85
  };
51
86
  }
52
- if (typeof auth.access !== "string" || !auth.access) {
87
+ if (typeof auth.access !== 'string' || !auth.access) {
53
88
  return {
54
89
  ...base,
55
- status: "unavailable",
90
+ status: 'unavailable',
56
91
  checkedAt,
57
- note: "missing oauth access token",
92
+ note: 'missing oauth access token',
58
93
  };
59
94
  }
60
- const response = await fetchWithTimeout("https://api.anthropic.com/api/oauth/usage", {
61
- method: "GET",
62
- headers: {
63
- Accept: "application/json",
64
- Authorization: `Bearer ${auth.access}`,
65
- "Content-Type": "application/json",
66
- "User-Agent": "opencode-quota-sidebar",
67
- "anthropic-beta": ANTHROPIC_OAUTH_USAGE_BETA,
68
- },
69
- }, config.quota.requestTimeoutMs).catch(swallow("fetchAnthropicQuota:usage"));
95
+ const { response, errorNote } = await fetchAnthropicUsage(auth.access, config.quota.requestTimeoutMs);
70
96
  if (!response) {
71
97
  return {
72
98
  ...base,
73
- status: "error",
99
+ status: 'error',
74
100
  checkedAt,
75
- note: "network request failed",
101
+ note: errorNote || 'network request failed',
76
102
  };
77
103
  }
78
104
  if (!response.ok) {
79
105
  return {
80
106
  ...base,
81
- status: "error",
107
+ status: 'error',
82
108
  checkedAt,
83
109
  note: `http ${response.status}`,
84
110
  };
85
111
  }
86
- const payload = await response
87
- .json()
88
- .catch(swallow("fetchAnthropicQuota:json"));
112
+ const payload = await response.json().catch(() => undefined);
89
113
  const usage = asRecord(payload);
90
114
  if (!usage) {
91
115
  return {
92
116
  ...base,
93
- status: "error",
117
+ status: 'error',
94
118
  checkedAt,
95
- note: "invalid response",
119
+ note: 'invalid response',
96
120
  };
97
121
  }
98
122
  const windows = ANTHROPIC_WINDOW_FIELDS.map(([field, label]) => parseAnthropicWindow(usage[field], label)).filter((window) => Boolean(window));
99
123
  const primary = windows[0];
100
124
  return {
101
125
  ...base,
102
- status: primary ? "ok" : "error",
126
+ status: primary ? 'ok' : 'error',
103
127
  checkedAt,
104
128
  usedPercent: primary?.usedPercent,
105
129
  remainingPercent: primary?.remainingPercent,
106
130
  resetAt: primary?.resetAt,
107
- note: primary ? undefined : "missing quota fields",
131
+ note: primary ? undefined : 'missing quota fields',
108
132
  windows: windows.length > 0 ? windows : undefined,
109
133
  };
110
134
  }
111
135
  export const anthropicAdapter = {
112
- id: "anthropic",
113
- label: "Anthropic",
114
- shortLabel: "Anthropic",
136
+ id: 'anthropic',
137
+ label: 'Anthropic',
138
+ shortLabel: 'Anthropic',
115
139
  sortOrder: 30,
116
- matchScore: ({ providerID }) => (providerID === "anthropic" ? 80 : 0),
117
- isEnabled: (config) => configuredProviderEnabled(config.quota, "anthropic", config.quota.includeAnthropic),
140
+ matchScore: ({ providerID }) => (providerID === 'anthropic' ? 80 : 0),
141
+ isEnabled: (config) => configuredProviderEnabled(config.quota, 'anthropic', config.quota.includeAnthropic),
118
142
  fetch: fetchAnthropicQuota,
119
143
  };
@@ -42,19 +42,30 @@ function windowResetAt(win, fallback) {
42
42
  return undefined;
43
43
  return new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
44
44
  }
45
- function parseOpenAIWindow(win, fallbackLabel) {
45
+ function parseOpenAIWindow(win, fallbackLabel, labelPrefix = '') {
46
46
  const usedPercent = normalizeOpenAIQuotaPercent(win.used_percent);
47
47
  const remainingPercent = normalizeOpenAIQuotaPercent(win.remaining_percent) ??
48
48
  (usedPercent === undefined ? undefined : 100 - usedPercent);
49
49
  if (remainingPercent === undefined)
50
50
  return undefined;
51
51
  return {
52
- label: windowLabel(win, fallbackLabel),
52
+ label: `${labelPrefix}${windowLabel(win, fallbackLabel)}`.trim(),
53
53
  remainingPercent,
54
54
  usedPercent,
55
55
  resetAt: windowResetAt(win),
56
56
  };
57
57
  }
58
+ function additionalRateLimitPrefix(limitName, meteredFeature) {
59
+ if (meteredFeature === 'codex_bengalfox')
60
+ return 'Spark ';
61
+ if (typeof meteredFeature === 'string' && meteredFeature)
62
+ return undefined;
63
+ if (typeof limitName !== 'string' || !limitName)
64
+ return undefined;
65
+ if (/codex-spark/i.test(limitName))
66
+ return 'Spark ';
67
+ return undefined;
68
+ }
58
69
  async function fetchOpenAIQuota(ctx) {
59
70
  const checkedAt = Date.now();
60
71
  const base = {
@@ -195,6 +206,31 @@ async function fetchOpenAIQuota(ctx) {
195
206
  if (secondaryWin)
196
207
  windows.push(secondaryWin);
197
208
  }
209
+ const additionalRateLimits = Array.isArray(payload.additional_rate_limits)
210
+ ? payload.additional_rate_limits
211
+ : [];
212
+ for (const item of additionalRateLimits) {
213
+ if (!isRecord(item))
214
+ continue;
215
+ const prefix = additionalRateLimitPrefix(item.limit_name, item.metered_feature);
216
+ if (!prefix)
217
+ continue;
218
+ const itemRateLimit = isRecord(item.rate_limit)
219
+ ? item.rate_limit
220
+ : undefined;
221
+ if (!itemRateLimit)
222
+ continue;
223
+ if (isRecord(itemRateLimit.primary_window)) {
224
+ const primaryWin = parseOpenAIWindow(itemRateLimit.primary_window, '', prefix);
225
+ if (primaryWin)
226
+ windows.push(primaryWin);
227
+ }
228
+ if (isRecord(itemRateLimit.secondary_window)) {
229
+ const secondaryWin = parseOpenAIWindow(itemRateLimit.secondary_window, 'Weekly', prefix);
230
+ if (secondaryWin)
231
+ windows.push(secondaryWin);
232
+ }
233
+ }
198
234
  return {
199
235
  ...base,
200
236
  status: remainingPercent === undefined ? 'error' : 'ok',
@@ -6,7 +6,6 @@ import { openaiAdapter } from './core/openai.js';
6
6
  import { zhipuCodingPlanAdapter } from './core/zhipu_coding_plan.js';
7
7
  import { QuotaProviderRegistry } from './registry.js';
8
8
  import { rightCodeAdapter } from './third_party/rightcode.js';
9
- import { xyaiAdapter } from './third_party/xyai.js';
10
9
  export declare function createDefaultProviderRegistry(): QuotaProviderRegistry;
11
- export { anthropicAdapter, copilotAdapter, kimiForCodingAdapter, minimaxCnCodingPlanAdapter, openaiAdapter, rightCodeAdapter, xyaiAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
10
+ export { anthropicAdapter, copilotAdapter, kimiForCodingAdapter, minimaxCnCodingPlanAdapter, openaiAdapter, rightCodeAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
12
11
  export type { AuthUpdate, AuthValue, ProviderResolveContext, QuotaFetchContext, QuotaProviderAdapter, RefreshedOAuthAuth, } from './types.js';
@@ -6,11 +6,9 @@ import { openaiAdapter } from './core/openai.js';
6
6
  import { zhipuCodingPlanAdapter } from './core/zhipu_coding_plan.js';
7
7
  import { QuotaProviderRegistry } from './registry.js';
8
8
  import { rightCodeAdapter } from './third_party/rightcode.js';
9
- import { xyaiAdapter } from './third_party/xyai.js';
10
9
  export function createDefaultProviderRegistry() {
11
10
  const registry = new QuotaProviderRegistry();
12
11
  registry.register(rightCodeAdapter);
13
- registry.register(xyaiAdapter);
14
12
  registry.register(kimiForCodingAdapter);
15
13
  registry.register(zhipuCodingPlanAdapter);
16
14
  registry.register(minimaxCnCodingPlanAdapter);
@@ -19,4 +17,4 @@ export function createDefaultProviderRegistry() {
19
17
  registry.register(anthropicAdapter);
20
18
  return registry;
21
19
  }
22
- export { anthropicAdapter, copilotAdapter, kimiForCodingAdapter, minimaxCnCodingPlanAdapter, openaiAdapter, rightCodeAdapter, xyaiAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
20
+ export { anthropicAdapter, copilotAdapter, kimiForCodingAdapter, minimaxCnCodingPlanAdapter, openaiAdapter, rightCodeAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
package/dist/quota.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { AuthUpdate, AuthValue } from "./providers/types.js";
2
- import type { QuotaSidebarConfig, QuotaSnapshot } from "./types.js";
1
+ import type { AuthUpdate, AuthValue } from './providers/types.js';
2
+ import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
3
3
  export declare function quotaSort(left: QuotaSnapshot, right: QuotaSnapshot): number;
4
4
  export declare function listDefaultQuotaProviderIDs(): string[];
5
5
  export declare function createQuotaRuntime(): {
@@ -24,6 +24,7 @@ export declare function createQuotaRuntime(): {
24
24
  };
25
25
  note?: string;
26
26
  windows?: import("./types.js").QuotaWindow[];
27
+ stale?: import("./types.js").QuotaStaleMeta;
27
28
  } | undefined>;
28
29
  };
29
30
  export declare function normalizeProviderID(providerID: string): string;
@@ -48,4 +49,5 @@ export declare function fetchQuotaSnapshot(providerID: string, authMap: Record<s
48
49
  };
49
50
  note?: string;
50
51
  windows?: import("./types.js").QuotaWindow[];
52
+ stale?: import("./types.js").QuotaStaleMeta;
51
53
  } | undefined>;
package/dist/quota.js CHANGED
@@ -1,14 +1,14 @@
1
- import fs from "node:fs/promises";
2
- import { isRecord, swallow } from "./helpers.js";
3
- import { createDefaultProviderRegistry } from "./providers/index.js";
4
- import { sanitizeBaseURL } from "./providers/common.js";
1
+ import fs from 'node:fs/promises';
2
+ import { isRecord, swallow } from './helpers.js';
3
+ import { createDefaultProviderRegistry } from './providers/index.js';
4
+ import { sanitizeBaseURL } from './providers/common.js';
5
5
  function resolveContext(providerID, providerOptions) {
6
6
  return { providerID, providerOptions };
7
7
  }
8
8
  function authCandidates(providerID, normalizedProviderID, adapterID) {
9
9
  const candidates = new Set([providerID]);
10
- if (adapterID === "github-copilot") {
11
- candidates.add("github-copilot-enterprise");
10
+ if (adapterID === 'github-copilot') {
11
+ candidates.add('github-copilot-enterprise');
12
12
  }
13
13
  candidates.add(normalizedProviderID);
14
14
  candidates.add(adapterID);
@@ -34,12 +34,12 @@ export function quotaSort(left, right) {
34
34
  export function listDefaultQuotaProviderIDs() {
35
35
  // Keep default report behavior stable for built-in subscription providers.
36
36
  return [
37
- "openai",
38
- "kimi-for-coding",
39
- "zhipuai-coding-plan",
40
- "minimax-cn-coding-plan",
41
- "github-copilot",
42
- "anthropic",
37
+ 'openai',
38
+ 'kimi-for-coding',
39
+ 'zhipuai-coding-plan',
40
+ 'minimax-cn-coding-plan',
41
+ 'github-copilot',
42
+ 'anthropic',
43
43
  ];
44
44
  }
45
45
  export function createQuotaRuntime() {
@@ -60,14 +60,11 @@ export function createQuotaRuntime() {
60
60
  keyBase = `${adapter.id}:${providerID}`;
61
61
  }
62
62
  // Some third-party adapters intentionally preserve provider-specific labels
63
- // (for example RC-openai or an XYAI alias) even when they share one adapter.
63
+ // (for example RC-openai) even when they share one adapter.
64
64
  // Keep the original provider identity in cache keys so same-host aliases with
65
65
  // different auth/config entries do not overwrite each other.
66
- if ((adapter?.id === "rightcode" &&
67
- normalizedProviderID.startsWith("rightcode-")) ||
68
- (adapter?.id === "xyai" &&
69
- providerID !== "xyai" &&
70
- providerID !== "xyai-vibe")) {
66
+ if (adapter?.id === 'rightcode' &&
67
+ normalizedProviderID.startsWith('rightcode-')) {
71
68
  keyBase = normalizedProviderID;
72
69
  }
73
70
  return baseURL ? `${keyBase}@${baseURL}` : keyBase;
@@ -116,16 +113,16 @@ export function quotaCacheKey(providerID, providerOptions) {
116
113
  }
117
114
  export async function loadAuthMap(authPath) {
118
115
  const parsed = await fs
119
- .readFile(authPath, "utf8")
116
+ .readFile(authPath, 'utf8')
120
117
  .then((value) => JSON.parse(value))
121
- .catch(swallow("loadAuthMap"));
118
+ .catch(swallow('loadAuthMap'));
122
119
  if (!isRecord(parsed))
123
120
  return {};
124
121
  return Object.entries(parsed).reduce((acc, [key, value]) => {
125
122
  if (!isRecord(value))
126
123
  return acc;
127
124
  const type = value.type;
128
- if (type !== "oauth" && type !== "api" && type !== "wellknown")
125
+ if (type !== 'oauth' && type !== 'api' && type !== 'wellknown')
129
126
  return acc;
130
127
  acc[key] = value;
131
128
  return acc;
@@ -1,4 +1,4 @@
1
- import type { QuotaSnapshot } from "./types.js";
1
+ import type { QuotaSnapshot } from './types.js';
2
2
  export declare function canonicalProviderID(providerID: string): string;
3
3
  export declare function displayShortLabel(providerID: string): string;
4
4
  export declare function quotaDisplayLabel(quota: QuotaSnapshot): string;