@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.
- package/CHANGELOG.md +0 -1
- package/README.md +163 -42
- package/README.zh-CN.md +163 -42
- package/SECURITY.md +1 -1
- package/dist/cli.d.ts +18 -0
- package/dist/cli.js +354 -0
- package/dist/cli_render.d.ts +17 -0
- package/dist/cli_render.js +292 -0
- package/dist/events.d.ts +1 -1
- package/dist/events.js +2 -2
- package/dist/format.d.ts +4 -0
- package/dist/format.js +391 -49
- package/dist/history_messages.d.ts +8 -0
- package/dist/history_messages.js +157 -0
- package/dist/history_usage.d.ts +93 -0
- package/dist/history_usage.js +251 -0
- package/dist/index.js +29 -4
- package/dist/period.d.ts +29 -1
- package/dist/period.js +187 -9
- package/dist/provider_catalog.d.ts +8 -0
- package/dist/provider_catalog.js +68 -0
- package/dist/providers/core/anthropic.d.ts +1 -1
- package/dist/providers/core/anthropic.js +69 -45
- package/dist/providers/core/openai.js +38 -2
- package/dist/providers/index.d.ts +1 -2
- package/dist/providers/index.js +1 -3
- package/dist/quota.d.ts +4 -2
- package/dist/quota.js +18 -21
- package/dist/quota_render.d.ts +1 -1
- package/dist/quota_render.js +23 -24
- package/dist/quota_service.d.ts +1 -0
- package/dist/quota_service.js +151 -19
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +4 -4
- package/dist/storage_dates.d.ts +1 -1
- package/dist/storage_dates.js +8 -5
- package/dist/storage_parse.js +23 -1
- package/dist/supported_quota.d.ts +4 -0
- package/dist/supported_quota.js +36 -0
- package/dist/title.js +21 -10
- package/dist/tools.d.ts +14 -3
- package/dist/tools.js +54 -2
- package/dist/tui.tsx +17 -6
- package/dist/tui_helpers.js +11 -6
- package/dist/types.d.ts +8 -0
- package/dist/usage.d.ts +18 -0
- package/dist/usage.js +93 -9
- package/dist/usage_service.d.ts +4 -1
- package/dist/usage_service.js +193 -189
- package/package.json +4 -1
- package/quota-sidebar.config.example.json +36 -45
- package/dist/providers/third_party/xyai.d.ts +0 -2
- package/dist/providers/third_party/xyai.js +0 -348
package/dist/period.js
CHANGED
|
@@ -1,14 +1,192 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
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
|
|
8
|
-
const
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
1
|
+
import type { QuotaProviderAdapter } from '../types.js';
|
|
2
2
|
export declare const anthropicAdapter: QuotaProviderAdapter;
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
[
|
|
6
|
-
[
|
|
7
|
-
[
|
|
8
|
-
[
|
|
9
|
-
[
|
|
10
|
-
[
|
|
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:
|
|
32
|
-
label:
|
|
33
|
-
shortLabel:
|
|
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:
|
|
74
|
+
status: 'unavailable',
|
|
40
75
|
checkedAt,
|
|
41
|
-
note:
|
|
76
|
+
note: 'auth not found',
|
|
42
77
|
};
|
|
43
78
|
}
|
|
44
|
-
if (auth.type !==
|
|
79
|
+
if (auth.type !== 'oauth') {
|
|
45
80
|
return {
|
|
46
81
|
...base,
|
|
47
|
-
status:
|
|
82
|
+
status: 'unsupported',
|
|
48
83
|
checkedAt,
|
|
49
|
-
note:
|
|
84
|
+
note: 'api key auth has no quota endpoint',
|
|
50
85
|
};
|
|
51
86
|
}
|
|
52
|
-
if (typeof auth.access !==
|
|
87
|
+
if (typeof auth.access !== 'string' || !auth.access) {
|
|
53
88
|
return {
|
|
54
89
|
...base,
|
|
55
|
-
status:
|
|
90
|
+
status: 'unavailable',
|
|
56
91
|
checkedAt,
|
|
57
|
-
note:
|
|
92
|
+
note: 'missing oauth access token',
|
|
58
93
|
};
|
|
59
94
|
}
|
|
60
|
-
const response = await
|
|
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:
|
|
99
|
+
status: 'error',
|
|
74
100
|
checkedAt,
|
|
75
|
-
note:
|
|
101
|
+
note: errorNote || 'network request failed',
|
|
76
102
|
};
|
|
77
103
|
}
|
|
78
104
|
if (!response.ok) {
|
|
79
105
|
return {
|
|
80
106
|
...base,
|
|
81
|
-
status:
|
|
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:
|
|
117
|
+
status: 'error',
|
|
94
118
|
checkedAt,
|
|
95
|
-
note:
|
|
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 ?
|
|
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 :
|
|
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:
|
|
113
|
-
label:
|
|
114
|
-
shortLabel:
|
|
136
|
+
id: 'anthropic',
|
|
137
|
+
label: 'Anthropic',
|
|
138
|
+
shortLabel: 'Anthropic',
|
|
115
139
|
sortOrder: 30,
|
|
116
|
-
matchScore: ({ providerID }) => (providerID ===
|
|
117
|
-
isEnabled: (config) => configuredProviderEnabled(config.quota,
|
|
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,
|
|
10
|
+
export { anthropicAdapter, copilotAdapter, kimiForCodingAdapter, minimaxCnCodingPlanAdapter, openaiAdapter, rightCodeAdapter, zhipuCodingPlanAdapter, QuotaProviderRegistry, };
|
|
12
11
|
export type { AuthUpdate, AuthValue, ProviderResolveContext, QuotaFetchContext, QuotaProviderAdapter, RefreshedOAuthAuth, } from './types.js';
|
package/dist/providers/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
2
|
-
import type { QuotaSidebarConfig, QuotaSnapshot } from
|
|
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
|
|
2
|
-
import { isRecord, swallow } from
|
|
3
|
-
import { createDefaultProviderRegistry } from
|
|
4
|
-
import { sanitizeBaseURL } from
|
|
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 ===
|
|
11
|
-
candidates.add(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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 (
|
|
67
|
-
normalizedProviderID.startsWith(
|
|
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,
|
|
116
|
+
.readFile(authPath, 'utf8')
|
|
120
117
|
.then((value) => JSON.parse(value))
|
|
121
|
-
.catch(swallow(
|
|
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 !==
|
|
125
|
+
if (type !== 'oauth' && type !== 'api' && type !== 'wellknown')
|
|
129
126
|
return acc;
|
|
130
127
|
acc[key] = value;
|
|
131
128
|
return acc;
|
package/dist/quota_render.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QuotaSnapshot } from
|
|
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;
|