@leo000001/opencode-quota-sidebar 1.0.0
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 +70 -0
- package/CONTRIBUTING.md +102 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/SECURITY.md +26 -0
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +22 -0
- package/dist/cost.d.ts +13 -0
- package/dist/cost.js +76 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.js +426 -0
- package/dist/helpers.d.ts +14 -0
- package/dist/helpers.js +50 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +699 -0
- package/dist/period.d.ts +1 -0
- package/dist/period.js +14 -0
- package/dist/providers/common.d.ts +24 -0
- package/dist/providers/common.js +114 -0
- package/dist/providers/core/anthropic.d.ts +2 -0
- package/dist/providers/core/anthropic.js +46 -0
- package/dist/providers/core/copilot.d.ts +2 -0
- package/dist/providers/core/copilot.js +117 -0
- package/dist/providers/core/openai.d.ts +2 -0
- package/dist/providers/core/openai.js +159 -0
- package/dist/providers/index.d.ts +8 -0
- package/dist/providers/index.js +14 -0
- package/dist/providers/registry.d.ts +9 -0
- package/dist/providers/registry.js +38 -0
- package/dist/providers/third_party/rightcode.d.ts +2 -0
- package/dist/providers/third_party/rightcode.js +230 -0
- package/dist/providers/types.d.ts +58 -0
- package/dist/providers/types.js +1 -0
- package/dist/quota.d.ts +49 -0
- package/dist/quota.js +116 -0
- package/dist/quota_render.d.ts +5 -0
- package/dist/quota_render.js +85 -0
- package/dist/storage.d.ts +32 -0
- package/dist/storage.js +328 -0
- package/dist/storage_chunks.d.ts +9 -0
- package/dist/storage_chunks.js +147 -0
- package/dist/storage_dates.d.ts +9 -0
- package/dist/storage_dates.js +88 -0
- package/dist/storage_parse.d.ts +4 -0
- package/dist/storage_parse.js +149 -0
- package/dist/storage_paths.d.ts +14 -0
- package/dist/storage_paths.js +31 -0
- package/dist/title.d.ts +8 -0
- package/dist/title.js +38 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.js +1 -0
- package/dist/usage.d.ts +51 -0
- package/dist/usage.js +243 -0
- package/package.json +68 -0
- package/quota-sidebar.config.example.json +25 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { isRecord, swallow } from '../../helpers.js';
|
|
2
|
+
import { asNumber, basePathPrefixes, configuredProviderEnabled, fetchWithTimeout, sanitizeBaseURL, toIso, } from '../common.js';
|
|
3
|
+
function isRightCodeBaseURL(value) {
|
|
4
|
+
const normalized = sanitizeBaseURL(value);
|
|
5
|
+
if (!normalized)
|
|
6
|
+
return false;
|
|
7
|
+
try {
|
|
8
|
+
const parsed = new URL(normalized);
|
|
9
|
+
if (parsed.protocol !== 'https:')
|
|
10
|
+
return false;
|
|
11
|
+
return parsed.host === 'www.right.codes' || parsed.host === 'right.codes';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function resolveApiKey(auth, providerOptions) {
|
|
18
|
+
const optionKey = providerOptions?.apiKey;
|
|
19
|
+
if (typeof optionKey === 'string' && optionKey)
|
|
20
|
+
return optionKey;
|
|
21
|
+
if (!auth)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (auth.type === 'api' && typeof auth.key === 'string' && auth.key) {
|
|
24
|
+
return auth.key;
|
|
25
|
+
}
|
|
26
|
+
if (auth.type === 'wellknown') {
|
|
27
|
+
if (typeof auth.key === 'string' && auth.key)
|
|
28
|
+
return auth.key;
|
|
29
|
+
if (typeof auth.token === 'string' && auth.token)
|
|
30
|
+
return auth.token;
|
|
31
|
+
}
|
|
32
|
+
if (auth.type === 'oauth' && typeof auth.access === 'string' && auth.access) {
|
|
33
|
+
return auth.access;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
function matchesSubscriptionPrefix(providerPrefixes, availablePrefixes) {
|
|
38
|
+
if (providerPrefixes.length === 0 || availablePrefixes.length === 0) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
for (const providerPrefix of providerPrefixes) {
|
|
42
|
+
for (const availablePrefix of availablePrefixes) {
|
|
43
|
+
if (providerPrefix === availablePrefix)
|
|
44
|
+
return true;
|
|
45
|
+
if (providerPrefix.startsWith(`${availablePrefix}/`))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
function formatQuotaValue(value) {
|
|
52
|
+
if (!Number.isFinite(value))
|
|
53
|
+
return '0';
|
|
54
|
+
const rounded = Number(value.toFixed(2));
|
|
55
|
+
return Number.isInteger(rounded) ? `${Math.trunc(rounded)}` : `${rounded}`;
|
|
56
|
+
}
|
|
57
|
+
function parseSubscription(value) {
|
|
58
|
+
const total = asNumber(value.total_quota);
|
|
59
|
+
const remaining = asNumber(value.remaining_quota);
|
|
60
|
+
// Ignore tiny/non-primary plans (badges, gifts, etc.).
|
|
61
|
+
if (total === undefined || remaining === undefined || total < 10) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
// RightCode daily quota semantics:
|
|
65
|
+
// - reset_today=true => normal same-day ratio: remaining / total
|
|
66
|
+
// - reset_today=false => include today's fresh quota: (remaining + total) / total
|
|
67
|
+
const resetToday = value.reset_today === true;
|
|
68
|
+
const dailyRemaining = resetToday ? remaining : remaining + total;
|
|
69
|
+
// Intentionally not using normalizePercent(): daily ratio can exceed 100%.
|
|
70
|
+
const remainingPercent = (dailyRemaining / total) * 100;
|
|
71
|
+
return {
|
|
72
|
+
name: typeof value.name === 'string' ? value.name : 'Subscription',
|
|
73
|
+
dailyTotal: total,
|
|
74
|
+
dailyRemaining,
|
|
75
|
+
remainingPercent,
|
|
76
|
+
expiresAt: toIso(value.expired_at),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function extractPrefixes(value) {
|
|
80
|
+
const raw = value.available_prefixes;
|
|
81
|
+
if (!Array.isArray(raw))
|
|
82
|
+
return [];
|
|
83
|
+
return raw.filter((item) => typeof item === 'string' && !!item);
|
|
84
|
+
}
|
|
85
|
+
async function fetchRightCodeQuota(ctx) {
|
|
86
|
+
const checkedAt = Date.now();
|
|
87
|
+
const sourceProviderID = typeof ctx.sourceProviderID === 'string' && ctx.sourceProviderID
|
|
88
|
+
? ctx.sourceProviderID
|
|
89
|
+
: ctx.providerID;
|
|
90
|
+
const shortLabel = sourceProviderID.startsWith('rightcode-')
|
|
91
|
+
? `RC-${sourceProviderID.slice('rightcode-'.length)}`
|
|
92
|
+
: 'RC';
|
|
93
|
+
const base = {
|
|
94
|
+
providerID: sourceProviderID,
|
|
95
|
+
adapterID: 'rightcode',
|
|
96
|
+
label: 'RightCode',
|
|
97
|
+
shortLabel,
|
|
98
|
+
sortOrder: 5,
|
|
99
|
+
};
|
|
100
|
+
const apiKey = resolveApiKey(ctx.auth, ctx.providerOptions);
|
|
101
|
+
if (!apiKey) {
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
status: 'unavailable',
|
|
105
|
+
checkedAt,
|
|
106
|
+
note: 'missing api key',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const response = await fetchWithTimeout('https://www.right.codes/account/summary', {
|
|
110
|
+
headers: {
|
|
111
|
+
Accept: 'application/json',
|
|
112
|
+
Authorization: `Bearer ${apiKey}`,
|
|
113
|
+
'User-Agent': 'opencode-quota-sidebar',
|
|
114
|
+
},
|
|
115
|
+
}, ctx.config.quota.requestTimeoutMs).catch(swallow('fetchRightCodeQuota'));
|
|
116
|
+
if (!response) {
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
status: 'error',
|
|
120
|
+
checkedAt,
|
|
121
|
+
note: 'network request failed',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
status: 'error',
|
|
128
|
+
checkedAt,
|
|
129
|
+
note: `http ${response.status}`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const payload = await response
|
|
133
|
+
.json()
|
|
134
|
+
.catch(swallow('fetchRightCodeQuota:json'));
|
|
135
|
+
if (!isRecord(payload)) {
|
|
136
|
+
return {
|
|
137
|
+
...base,
|
|
138
|
+
status: 'error',
|
|
139
|
+
checkedAt,
|
|
140
|
+
note: 'invalid response',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const balance = asNumber(payload.balance);
|
|
144
|
+
const providerPrefixes = basePathPrefixes(ctx.providerOptions?.baseURL);
|
|
145
|
+
if (providerPrefixes.length === 0) {
|
|
146
|
+
providerPrefixes.push(`/${ctx.providerID}`);
|
|
147
|
+
}
|
|
148
|
+
const subscriptions = Array.isArray(payload.subscriptions)
|
|
149
|
+
? payload.subscriptions.filter((item) => isRecord(item))
|
|
150
|
+
: [];
|
|
151
|
+
const matched = subscriptions
|
|
152
|
+
.filter((subscription) => {
|
|
153
|
+
const available = extractPrefixes(subscription);
|
|
154
|
+
return matchesSubscriptionPrefix(providerPrefixes, available);
|
|
155
|
+
})
|
|
156
|
+
.map((subscription) => parseSubscription(subscription))
|
|
157
|
+
.filter((subscription) => Boolean(subscription));
|
|
158
|
+
if (matched.length > 0) {
|
|
159
|
+
const dailyTotal = matched.reduce((sum, subscription) => sum + subscription.dailyTotal, 0);
|
|
160
|
+
const dailyRemaining = matched.reduce((sum, subscription) => sum + subscription.dailyRemaining, 0);
|
|
161
|
+
const dailyPercent = dailyTotal > 0 ? (dailyRemaining / dailyTotal) * 100 : undefined;
|
|
162
|
+
const expiry = matched.reduce((acc, subscription) => {
|
|
163
|
+
if (!subscription.expiresAt)
|
|
164
|
+
return acc;
|
|
165
|
+
const current = Date.parse(subscription.expiresAt);
|
|
166
|
+
if (Number.isNaN(current))
|
|
167
|
+
return acc;
|
|
168
|
+
if (!acc)
|
|
169
|
+
return subscription.expiresAt;
|
|
170
|
+
const existing = Date.parse(acc);
|
|
171
|
+
if (Number.isNaN(existing) || current < existing) {
|
|
172
|
+
return subscription.expiresAt;
|
|
173
|
+
}
|
|
174
|
+
return acc;
|
|
175
|
+
}, undefined);
|
|
176
|
+
const windows = [
|
|
177
|
+
{
|
|
178
|
+
label: `Daily $${formatQuotaValue(dailyRemaining)}/$${formatQuotaValue(dailyTotal)}`,
|
|
179
|
+
showPercent: false,
|
|
180
|
+
remainingPercent: dailyPercent,
|
|
181
|
+
resetAt: expiry,
|
|
182
|
+
resetLabel: 'Exp',
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
const names = matched.map((subscription) => subscription.name).join(', ');
|
|
186
|
+
return {
|
|
187
|
+
...base,
|
|
188
|
+
status: dailyPercent === undefined ? 'error' : 'ok',
|
|
189
|
+
checkedAt,
|
|
190
|
+
remainingPercent: dailyPercent,
|
|
191
|
+
balance: balance === undefined
|
|
192
|
+
? undefined
|
|
193
|
+
: {
|
|
194
|
+
amount: balance,
|
|
195
|
+
currency: '$',
|
|
196
|
+
},
|
|
197
|
+
windows,
|
|
198
|
+
note: dailyPercent === undefined
|
|
199
|
+
? 'matched subscription has no daily quota fields'
|
|
200
|
+
: `subscription daily quota: ${names}`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (balance !== undefined) {
|
|
204
|
+
return {
|
|
205
|
+
...base,
|
|
206
|
+
status: 'ok',
|
|
207
|
+
checkedAt,
|
|
208
|
+
balance: {
|
|
209
|
+
amount: balance,
|
|
210
|
+
currency: '$',
|
|
211
|
+
},
|
|
212
|
+
note: 'no matching subscription for provider prefix',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
...base,
|
|
217
|
+
status: 'error',
|
|
218
|
+
checkedAt,
|
|
219
|
+
note: 'missing balance and subscription fields',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export const rightCodeAdapter = {
|
|
223
|
+
id: 'rightcode',
|
|
224
|
+
label: 'RightCode',
|
|
225
|
+
shortLabel: 'RC',
|
|
226
|
+
sortOrder: 5,
|
|
227
|
+
matchScore: ({ providerOptions }) => isRightCodeBaseURL(providerOptions?.baseURL) ? 100 : 0,
|
|
228
|
+
isEnabled: (config) => configuredProviderEnabled(config.quota, 'rightcode', true),
|
|
229
|
+
fetch: fetchRightCodeQuota,
|
|
230
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { QuotaSidebarConfig, QuotaSnapshot } from '../types.js';
|
|
2
|
+
export type OAuthAuth = {
|
|
3
|
+
type: 'oauth';
|
|
4
|
+
access?: string;
|
|
5
|
+
refresh?: string;
|
|
6
|
+
expires?: number;
|
|
7
|
+
accountId?: string;
|
|
8
|
+
enterpriseUrl?: string;
|
|
9
|
+
};
|
|
10
|
+
export type ApiAuth = {
|
|
11
|
+
type: 'api';
|
|
12
|
+
key?: string;
|
|
13
|
+
};
|
|
14
|
+
export type WellKnownAuth = {
|
|
15
|
+
type: 'wellknown';
|
|
16
|
+
key?: string;
|
|
17
|
+
token?: string;
|
|
18
|
+
};
|
|
19
|
+
export type AuthValue = OAuthAuth | ApiAuth | WellKnownAuth;
|
|
20
|
+
export type RefreshedOAuthAuth = {
|
|
21
|
+
type: 'oauth';
|
|
22
|
+
access: string;
|
|
23
|
+
refresh: string;
|
|
24
|
+
expires: number;
|
|
25
|
+
accountId?: string;
|
|
26
|
+
enterpriseUrl?: string;
|
|
27
|
+
};
|
|
28
|
+
export type AuthUpdate = (providerID: string, auth: RefreshedOAuthAuth) => Promise<void>;
|
|
29
|
+
export type ProviderResolveContext = {
|
|
30
|
+
providerID: string;
|
|
31
|
+
providerOptions?: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
export type QuotaFetchContext = {
|
|
34
|
+
/** Original provider ID before normalization (useful for adapter variants). */
|
|
35
|
+
sourceProviderID?: string;
|
|
36
|
+
providerID: string;
|
|
37
|
+
providerOptions?: Record<string, unknown>;
|
|
38
|
+
auth: AuthValue | undefined;
|
|
39
|
+
config: QuotaSidebarConfig;
|
|
40
|
+
updateAuth?: AuthUpdate;
|
|
41
|
+
};
|
|
42
|
+
export type QuotaProviderAdapter = {
|
|
43
|
+
id: string;
|
|
44
|
+
label: string;
|
|
45
|
+
shortLabel: string;
|
|
46
|
+
sortOrder: number;
|
|
47
|
+
/** Higher score wins. 0 means no match. */
|
|
48
|
+
matchScore: (ctx: ProviderResolveContext) => number;
|
|
49
|
+
/** Resolve provider ID variants to a canonical id when needed. */
|
|
50
|
+
normalizeID?: (providerID: string) => string | undefined;
|
|
51
|
+
/** Provider-specific enable switch (supports backward-compatible flags). */
|
|
52
|
+
isEnabled: (config: QuotaSidebarConfig) => boolean;
|
|
53
|
+
fetch: (ctx: QuotaFetchContext) => Promise<QuotaSnapshot>;
|
|
54
|
+
};
|
|
55
|
+
export type ProviderMatch = {
|
|
56
|
+
adapter: QuotaProviderAdapter;
|
|
57
|
+
score: number;
|
|
58
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/quota.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AuthUpdate, AuthValue } from './providers/types.js';
|
|
2
|
+
import type { QuotaSidebarConfig, QuotaSnapshot } from './types.js';
|
|
3
|
+
export declare function quotaSort(left: QuotaSnapshot, right: QuotaSnapshot): number;
|
|
4
|
+
export declare function listDefaultQuotaProviderIDs(): string[];
|
|
5
|
+
export declare function createQuotaRuntime(): {
|
|
6
|
+
normalizeProviderID: (providerID: string) => string;
|
|
7
|
+
resolveQuotaAdapter: (providerID: string, providerOptions?: Record<string, unknown>) => import("./providers/types.js").QuotaProviderAdapter | undefined;
|
|
8
|
+
quotaCacheKey: (providerID: string, providerOptions?: Record<string, unknown>) => string;
|
|
9
|
+
fetchQuotaSnapshot: (providerID: string, authMap: Record<string, AuthValue>, config: QuotaSidebarConfig, updateAuth?: AuthUpdate, providerOptions?: Record<string, unknown>) => Promise<{
|
|
10
|
+
adapterID: string;
|
|
11
|
+
shortLabel: string;
|
|
12
|
+
sortOrder: number;
|
|
13
|
+
label: string;
|
|
14
|
+
providerID: string;
|
|
15
|
+
status: import("./types.js").QuotaStatus;
|
|
16
|
+
checkedAt: number;
|
|
17
|
+
remainingPercent?: number;
|
|
18
|
+
usedPercent?: number;
|
|
19
|
+
resetAt?: string;
|
|
20
|
+
balance?: {
|
|
21
|
+
amount: number;
|
|
22
|
+
currency: string;
|
|
23
|
+
};
|
|
24
|
+
note?: string;
|
|
25
|
+
windows?: import("./types.js").QuotaWindow[];
|
|
26
|
+
} | undefined>;
|
|
27
|
+
};
|
|
28
|
+
export declare function normalizeProviderID(providerID: string): string;
|
|
29
|
+
export declare function resolveQuotaAdapter(providerID: string, providerOptions?: Record<string, unknown>): import("./providers/types.js").QuotaProviderAdapter | undefined;
|
|
30
|
+
export declare function quotaCacheKey(providerID: string, providerOptions?: Record<string, unknown>): string;
|
|
31
|
+
export declare function loadAuthMap(authPath: string): Promise<Record<string, AuthValue>>;
|
|
32
|
+
export declare function fetchQuotaSnapshot(providerID: string, authMap: Record<string, AuthValue>, config: QuotaSidebarConfig, updateAuth?: AuthUpdate, providerOptions?: Record<string, unknown>): Promise<{
|
|
33
|
+
adapterID: string;
|
|
34
|
+
shortLabel: string;
|
|
35
|
+
sortOrder: number;
|
|
36
|
+
label: string;
|
|
37
|
+
providerID: string;
|
|
38
|
+
status: import("./types.js").QuotaStatus;
|
|
39
|
+
checkedAt: number;
|
|
40
|
+
remainingPercent?: number;
|
|
41
|
+
usedPercent?: number;
|
|
42
|
+
resetAt?: string;
|
|
43
|
+
balance?: {
|
|
44
|
+
amount: number;
|
|
45
|
+
currency: string;
|
|
46
|
+
};
|
|
47
|
+
note?: string;
|
|
48
|
+
windows?: import("./types.js").QuotaWindow[];
|
|
49
|
+
} | undefined>;
|
package/dist/quota.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
function resolveContext(providerID, providerOptions) {
|
|
6
|
+
return { providerID, providerOptions };
|
|
7
|
+
}
|
|
8
|
+
function authCandidates(providerID, normalizedProviderID, adapterID) {
|
|
9
|
+
const candidates = new Set([
|
|
10
|
+
providerID,
|
|
11
|
+
normalizedProviderID,
|
|
12
|
+
adapterID,
|
|
13
|
+
]);
|
|
14
|
+
if (adapterID === 'github-copilot') {
|
|
15
|
+
candidates.add('github-copilot-enterprise');
|
|
16
|
+
}
|
|
17
|
+
return [...candidates];
|
|
18
|
+
}
|
|
19
|
+
function pickAuth(providerID, normalizedProviderID, adapterID, authMap) {
|
|
20
|
+
for (const key of authCandidates(providerID, normalizedProviderID, adapterID)) {
|
|
21
|
+
const auth = authMap[key];
|
|
22
|
+
if (auth)
|
|
23
|
+
return auth;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
export function quotaSort(left, right) {
|
|
28
|
+
const leftOrder = left.sortOrder ?? 99;
|
|
29
|
+
const rightOrder = right.sortOrder ?? 99;
|
|
30
|
+
if (leftOrder !== rightOrder)
|
|
31
|
+
return leftOrder - rightOrder;
|
|
32
|
+
const leftKey = left.adapterID || left.providerID;
|
|
33
|
+
const rightKey = right.adapterID || right.providerID;
|
|
34
|
+
return leftKey.localeCompare(rightKey);
|
|
35
|
+
}
|
|
36
|
+
export function listDefaultQuotaProviderIDs() {
|
|
37
|
+
// Keep default report behavior stable for built-in subscription providers.
|
|
38
|
+
return ['openai', 'github-copilot', 'anthropic'];
|
|
39
|
+
}
|
|
40
|
+
export function createQuotaRuntime() {
|
|
41
|
+
const providerRegistry = createDefaultProviderRegistry();
|
|
42
|
+
const normalizeProviderID = (providerID) => providerRegistry.normalizeProviderID(providerID);
|
|
43
|
+
const resolveQuotaAdapter = (providerID, providerOptions) => {
|
|
44
|
+
return providerRegistry.resolve(resolveContext(providerID, providerOptions));
|
|
45
|
+
};
|
|
46
|
+
const quotaCacheKey = (providerID, providerOptions) => {
|
|
47
|
+
const adapter = resolveQuotaAdapter(providerID, providerOptions);
|
|
48
|
+
const normalizedProviderID = normalizeProviderID(providerID);
|
|
49
|
+
const baseURL = sanitizeBaseURL(providerOptions?.baseURL);
|
|
50
|
+
const keyBase = adapter?.id || normalizedProviderID;
|
|
51
|
+
return baseURL ? `${keyBase}@${baseURL}` : keyBase;
|
|
52
|
+
};
|
|
53
|
+
const fetchQuotaSnapshot = async (providerID, authMap, config, updateAuth, providerOptions) => {
|
|
54
|
+
const context = resolveContext(providerID, providerOptions);
|
|
55
|
+
const adapter = providerRegistry.resolve(context);
|
|
56
|
+
if (!adapter)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (!adapter.isEnabled(config))
|
|
59
|
+
return undefined;
|
|
60
|
+
const normalizedProviderID = adapter.normalizeID?.(providerID) ?? normalizeProviderID(providerID);
|
|
61
|
+
const auth = pickAuth(providerID, normalizedProviderID, adapter.id, authMap);
|
|
62
|
+
const snapshot = await adapter.fetch({
|
|
63
|
+
sourceProviderID: providerID,
|
|
64
|
+
providerID: normalizedProviderID,
|
|
65
|
+
providerOptions,
|
|
66
|
+
auth,
|
|
67
|
+
config,
|
|
68
|
+
updateAuth,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
...snapshot,
|
|
72
|
+
adapterID: snapshot.adapterID || adapter.id,
|
|
73
|
+
shortLabel: snapshot.shortLabel || adapter.shortLabel,
|
|
74
|
+
sortOrder: snapshot.sortOrder ?? adapter.sortOrder,
|
|
75
|
+
label: snapshot.label || adapter.label,
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
return {
|
|
79
|
+
normalizeProviderID,
|
|
80
|
+
resolveQuotaAdapter,
|
|
81
|
+
quotaCacheKey,
|
|
82
|
+
fetchQuotaSnapshot,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function withRuntime(fn) {
|
|
86
|
+
return fn(createQuotaRuntime());
|
|
87
|
+
}
|
|
88
|
+
export function normalizeProviderID(providerID) {
|
|
89
|
+
return withRuntime((runtime) => runtime.normalizeProviderID(providerID));
|
|
90
|
+
}
|
|
91
|
+
export function resolveQuotaAdapter(providerID, providerOptions) {
|
|
92
|
+
return withRuntime((runtime) => runtime.resolveQuotaAdapter(providerID, providerOptions));
|
|
93
|
+
}
|
|
94
|
+
export function quotaCacheKey(providerID, providerOptions) {
|
|
95
|
+
return withRuntime((runtime) => runtime.quotaCacheKey(providerID, providerOptions));
|
|
96
|
+
}
|
|
97
|
+
export async function loadAuthMap(authPath) {
|
|
98
|
+
const parsed = await fs
|
|
99
|
+
.readFile(authPath, 'utf8')
|
|
100
|
+
.then((value) => JSON.parse(value))
|
|
101
|
+
.catch(swallow('loadAuthMap'));
|
|
102
|
+
if (!isRecord(parsed))
|
|
103
|
+
return {};
|
|
104
|
+
return Object.entries(parsed).reduce((acc, [key, value]) => {
|
|
105
|
+
if (!isRecord(value))
|
|
106
|
+
return acc;
|
|
107
|
+
const type = value.type;
|
|
108
|
+
if (type !== 'oauth' && type !== 'api' && type !== 'wellknown')
|
|
109
|
+
return acc;
|
|
110
|
+
acc[key] = value;
|
|
111
|
+
return acc;
|
|
112
|
+
}, {});
|
|
113
|
+
}
|
|
114
|
+
export async function fetchQuotaSnapshot(providerID, authMap, config, updateAuth, providerOptions) {
|
|
115
|
+
return withRuntime((runtime) => runtime.fetchQuotaSnapshot(providerID, authMap, config, updateAuth, providerOptions));
|
|
116
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { QuotaSnapshot } from './types.js';
|
|
2
|
+
export declare function canonicalProviderID(providerID: string): string;
|
|
3
|
+
export declare function displayShortLabel(providerID: string): string;
|
|
4
|
+
export declare function quotaDisplayLabel(quota: QuotaSnapshot): string;
|
|
5
|
+
export declare function collapseQuotaSnapshots(quotas: QuotaSnapshot[]): QuotaSnapshot[];
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const PROVIDER_SHORT_LABELS = {
|
|
2
|
+
openai: 'OpenAI',
|
|
3
|
+
'github-copilot': 'Copilot',
|
|
4
|
+
anthropic: 'Anthropic',
|
|
5
|
+
rightcode: 'RC',
|
|
6
|
+
};
|
|
7
|
+
export function canonicalProviderID(providerID) {
|
|
8
|
+
if (providerID.startsWith('github-copilot'))
|
|
9
|
+
return 'github-copilot';
|
|
10
|
+
return providerID;
|
|
11
|
+
}
|
|
12
|
+
export function displayShortLabel(providerID) {
|
|
13
|
+
const canonical = canonicalProviderID(providerID);
|
|
14
|
+
const direct = PROVIDER_SHORT_LABELS[canonical];
|
|
15
|
+
if (direct)
|
|
16
|
+
return direct;
|
|
17
|
+
if (canonical.startsWith('rightcode-')) {
|
|
18
|
+
return `RC-${canonical.slice('rightcode-'.length)}`;
|
|
19
|
+
}
|
|
20
|
+
return providerID;
|
|
21
|
+
}
|
|
22
|
+
export function quotaDisplayLabel(quota) {
|
|
23
|
+
if (quota.shortLabel)
|
|
24
|
+
return quota.shortLabel;
|
|
25
|
+
if (quota.adapterID) {
|
|
26
|
+
const adapterLabel = displayShortLabel(quota.adapterID);
|
|
27
|
+
if (adapterLabel !== quota.adapterID)
|
|
28
|
+
return adapterLabel;
|
|
29
|
+
}
|
|
30
|
+
return displayShortLabel(quota.providerID);
|
|
31
|
+
}
|
|
32
|
+
function quotaKey(quota) {
|
|
33
|
+
if (quota.adapterID === 'rightcode')
|
|
34
|
+
return `rightcode:${quota.providerID}`;
|
|
35
|
+
return `${quota.adapterID || quota.providerID}:${quota.providerID}`;
|
|
36
|
+
}
|
|
37
|
+
function quotaScore(quota) {
|
|
38
|
+
let score = 0;
|
|
39
|
+
if (quota.status === 'ok')
|
|
40
|
+
score += 10;
|
|
41
|
+
if (quota.windows && quota.windows.length > 0) {
|
|
42
|
+
score += 5 + quota.windows.length;
|
|
43
|
+
}
|
|
44
|
+
if (quota.balance)
|
|
45
|
+
score += 3;
|
|
46
|
+
if (quota.remainingPercent !== undefined)
|
|
47
|
+
score += 1;
|
|
48
|
+
return score;
|
|
49
|
+
}
|
|
50
|
+
export function collapseQuotaSnapshots(quotas) {
|
|
51
|
+
const grouped = new Map();
|
|
52
|
+
const hasRightCodeBase = quotas.some((quota) => quota.adapterID === 'rightcode' && quotaDisplayLabel(quota) === 'RC');
|
|
53
|
+
for (const quota of quotas) {
|
|
54
|
+
// If both RC (balance) and RC-variant (subscription) exist,
|
|
55
|
+
// treat balance as owned by RC.
|
|
56
|
+
const normalizedQuota = hasRightCodeBase &&
|
|
57
|
+
quota.adapterID === 'rightcode' &&
|
|
58
|
+
quotaDisplayLabel(quota).startsWith('RC-')
|
|
59
|
+
? { ...quota, balance: undefined }
|
|
60
|
+
: quota;
|
|
61
|
+
const key = quotaKey(normalizedQuota);
|
|
62
|
+
const existing = grouped.get(key);
|
|
63
|
+
if (!existing) {
|
|
64
|
+
grouped.set(key, normalizedQuota);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const primary = quotaScore(normalizedQuota) >= quotaScore(existing)
|
|
68
|
+
? normalizedQuota
|
|
69
|
+
: existing;
|
|
70
|
+
const secondary = primary === normalizedQuota ? existing : normalizedQuota;
|
|
71
|
+
grouped.set(key, {
|
|
72
|
+
...primary,
|
|
73
|
+
windows: primary.windows && primary.windows.length > 0
|
|
74
|
+
? primary.windows
|
|
75
|
+
: secondary.windows,
|
|
76
|
+
balance: primary.balance || secondary.balance,
|
|
77
|
+
remainingPercent: primary.remainingPercent !== undefined
|
|
78
|
+
? primary.remainingPercent
|
|
79
|
+
: secondary.remainingPercent,
|
|
80
|
+
resetAt: primary.resetAt || secondary.resetAt,
|
|
81
|
+
note: primary.note || secondary.note,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return [...grouped.values()];
|
|
85
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { dateKeyFromTimestamp, normalizeTimestampMs } from './storage_dates.js';
|
|
2
|
+
import { authFilePath, resolveOpencodeDataDir, stateFilePath } from './storage_paths.js';
|
|
3
|
+
import type { QuotaSidebarConfig, QuotaSidebarState, SessionState } from './types.js';
|
|
4
|
+
export { authFilePath, dateKeyFromTimestamp, normalizeTimestampMs, resolveOpencodeDataDir, stateFilePath, };
|
|
5
|
+
export declare const defaultConfig: QuotaSidebarConfig;
|
|
6
|
+
export declare function defaultState(): QuotaSidebarState;
|
|
7
|
+
export declare function loadConfig(paths: string[]): Promise<QuotaSidebarConfig>;
|
|
8
|
+
export declare function loadState(statePath: string): Promise<QuotaSidebarState>;
|
|
9
|
+
/**
|
|
10
|
+
* H1 fix: when dirtyDateKeys is empty and writeAll is not set, skip chunk writes entirely.
|
|
11
|
+
* M11 fix: only iterate sessions belonging to dirty date keys (not all sessions).
|
|
12
|
+
* M4 fix: atomic writes via safeWriteFile.
|
|
13
|
+
* P4 fix: sessionDateMap dirty flag tracked externally.
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveState(statePath: string, state: QuotaSidebarState, options?: {
|
|
16
|
+
dirtyDateKeys?: string[];
|
|
17
|
+
writeAll?: boolean;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* M2 fix: evict sessions older than retentionDays from memory.
|
|
21
|
+
* Chunk files remain on disk for historical range scans.
|
|
22
|
+
*/
|
|
23
|
+
export declare function evictOldSessions(state: QuotaSidebarState, retentionDays: number): number;
|
|
24
|
+
/**
|
|
25
|
+
* M9 fix: scan from in-memory state first, only read disk for date keys
|
|
26
|
+
* not represented in memory.
|
|
27
|
+
*/
|
|
28
|
+
export declare function scanSessionsByCreatedRange(statePath: string, startAt: number, endAt?: number, memoryState?: QuotaSidebarState): Promise<{
|
|
29
|
+
sessionID: string;
|
|
30
|
+
dateKey: string;
|
|
31
|
+
state: SessionState;
|
|
32
|
+
}[]>;
|