@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
package/dist/period.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function periodStart(period: 'day' | 'week' | 'month'): number;
|
package/dist/period.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function periodStart(period) {
|
|
2
|
+
const now = new Date();
|
|
3
|
+
if (period === 'month') {
|
|
4
|
+
return new Date(now.getFullYear(), now.getMonth(), 1).getTime();
|
|
5
|
+
}
|
|
6
|
+
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();
|
|
12
|
+
}
|
|
13
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const OPENAI_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
2
|
+
export declare function fetchWithTimeout(url: string, init: RequestInit | undefined, timeoutMs: number): Promise<Response>;
|
|
3
|
+
export declare function asNumber(value: unknown): number | undefined;
|
|
4
|
+
export declare function normalizePercent(value: unknown): number | undefined;
|
|
5
|
+
export declare function toIso(value: unknown): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* Derive a human-readable window label from `limit_window_seconds`.
|
|
8
|
+
* Uses fallback label when the limit is missing.
|
|
9
|
+
*/
|
|
10
|
+
export declare function windowLabel(win: Record<string, unknown>, fallback?: string): string;
|
|
11
|
+
export declare function parseRateLimitWindow(win: Record<string, unknown>, fallbackLabel: string): {
|
|
12
|
+
label: string;
|
|
13
|
+
remainingPercent: number;
|
|
14
|
+
usedPercent: number | undefined;
|
|
15
|
+
resetAt: string | undefined;
|
|
16
|
+
} | undefined;
|
|
17
|
+
export declare function asRecord(value: unknown): Record<string, unknown> | undefined;
|
|
18
|
+
export declare function configuredProviderEnabled(config: {
|
|
19
|
+
providers?: Record<string, {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
}, adapterID: string, fallback?: boolean): boolean;
|
|
23
|
+
export declare function sanitizeBaseURL(value: unknown): string | undefined;
|
|
24
|
+
export declare function basePathPrefixes(value: unknown): string[];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { asNumber as asNumberShared, isRecord } from '../helpers.js';
|
|
2
|
+
// Public OAuth client ID embedded in the ChatGPT web app (not a private secret).
|
|
3
|
+
// Source: https://github.com/vbgate/opencode-mystatus (reverse-engineered from browser client).
|
|
4
|
+
// If OpenAI rotates this value, update it here or expose it via quota-sidebar.config.json.
|
|
5
|
+
export const OPENAI_OAUTH_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
|
|
6
|
+
export async function fetchWithTimeout(url, init, timeoutMs) {
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
|
+
try {
|
|
10
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
11
|
+
}
|
|
12
|
+
finally {
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function asNumber(value) {
|
|
17
|
+
return asNumberShared(value);
|
|
18
|
+
}
|
|
19
|
+
export function normalizePercent(value) {
|
|
20
|
+
const numeric = asNumber(value);
|
|
21
|
+
if (numeric === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
const expanded = numeric >= 0 && numeric <= 1 ? numeric * 100 : numeric;
|
|
24
|
+
if (Number.isNaN(expanded))
|
|
25
|
+
return undefined;
|
|
26
|
+
if (expanded < 0)
|
|
27
|
+
return 0;
|
|
28
|
+
if (expanded > 100)
|
|
29
|
+
return 100;
|
|
30
|
+
return expanded;
|
|
31
|
+
}
|
|
32
|
+
export function toIso(value) {
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
const time = Date.parse(value);
|
|
35
|
+
if (!Number.isNaN(time))
|
|
36
|
+
return new Date(time).toISOString();
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
const number = asNumber(value);
|
|
40
|
+
if (number === undefined)
|
|
41
|
+
return undefined;
|
|
42
|
+
const milliseconds = number > 10_000_000_000 ? number : number * 1000;
|
|
43
|
+
return new Date(milliseconds).toISOString();
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Derive a human-readable window label from `limit_window_seconds`.
|
|
47
|
+
* Uses fallback label when the limit is missing.
|
|
48
|
+
*/
|
|
49
|
+
export function windowLabel(win, fallback = '') {
|
|
50
|
+
const limitSec = asNumber(win.limit_window_seconds);
|
|
51
|
+
if (limitSec !== undefined && limitSec > 0) {
|
|
52
|
+
const hours = limitSec / 3600;
|
|
53
|
+
if (hours <= 24)
|
|
54
|
+
return `${Math.round(hours)}h`;
|
|
55
|
+
const days = hours / 24;
|
|
56
|
+
if (days <= 6)
|
|
57
|
+
return `${Math.round(days)}d`;
|
|
58
|
+
return 'Weekly';
|
|
59
|
+
}
|
|
60
|
+
return fallback || 'Window';
|
|
61
|
+
}
|
|
62
|
+
export function parseRateLimitWindow(win, fallbackLabel) {
|
|
63
|
+
const usedPercent = normalizePercent(win.used_percent);
|
|
64
|
+
const remainingPercent = normalizePercent(win.remaining_percent) ??
|
|
65
|
+
(usedPercent === undefined ? undefined : 100 - usedPercent);
|
|
66
|
+
if (remainingPercent === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
return {
|
|
69
|
+
label: windowLabel(win, fallbackLabel),
|
|
70
|
+
remainingPercent,
|
|
71
|
+
usedPercent,
|
|
72
|
+
resetAt: toIso(win.reset_at),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function asRecord(value) {
|
|
76
|
+
return isRecord(value) ? value : undefined;
|
|
77
|
+
}
|
|
78
|
+
export function configuredProviderEnabled(config, adapterID, fallback = true) {
|
|
79
|
+
const enabled = config.providers?.[adapterID]?.enabled;
|
|
80
|
+
if (typeof enabled === 'boolean')
|
|
81
|
+
return enabled;
|
|
82
|
+
return fallback;
|
|
83
|
+
}
|
|
84
|
+
export function sanitizeBaseURL(value) {
|
|
85
|
+
if (typeof value !== 'string' || !value)
|
|
86
|
+
return undefined;
|
|
87
|
+
try {
|
|
88
|
+
const parsed = new URL(value);
|
|
89
|
+
const pathname = parsed.pathname.replace(/\/+$/, '') || '/';
|
|
90
|
+
return `${parsed.origin}${pathname}`;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function basePathPrefixes(value) {
|
|
97
|
+
const sanitized = sanitizeBaseURL(value);
|
|
98
|
+
if (!sanitized)
|
|
99
|
+
return [];
|
|
100
|
+
try {
|
|
101
|
+
const parsed = new URL(sanitized);
|
|
102
|
+
const parts = parsed.pathname.split('/').filter(Boolean);
|
|
103
|
+
const prefixes = [];
|
|
104
|
+
for (let i = parts.length; i >= 1; i--) {
|
|
105
|
+
prefixes.push(`/${parts.slice(0, i).join('/')}`);
|
|
106
|
+
}
|
|
107
|
+
if (prefixes.length === 0)
|
|
108
|
+
prefixes.push('/');
|
|
109
|
+
return prefixes;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { configuredProviderEnabled } from '../common.js';
|
|
2
|
+
export const anthropicAdapter = {
|
|
3
|
+
id: 'anthropic',
|
|
4
|
+
label: 'Anthropic',
|
|
5
|
+
shortLabel: 'Anthropic',
|
|
6
|
+
sortOrder: 30,
|
|
7
|
+
matchScore: ({ providerID }) => (providerID === 'anthropic' ? 80 : 0),
|
|
8
|
+
isEnabled: (config) => configuredProviderEnabled(config.quota, 'anthropic', config.quota.includeAnthropic),
|
|
9
|
+
fetch: async ({ providerID, auth }) => {
|
|
10
|
+
const checkedAt = Date.now();
|
|
11
|
+
if (!auth) {
|
|
12
|
+
return {
|
|
13
|
+
providerID,
|
|
14
|
+
adapterID: 'anthropic',
|
|
15
|
+
label: 'Anthropic',
|
|
16
|
+
shortLabel: 'Anthropic',
|
|
17
|
+
sortOrder: 30,
|
|
18
|
+
status: 'unavailable',
|
|
19
|
+
checkedAt,
|
|
20
|
+
note: 'auth not found',
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (auth.type === 'api') {
|
|
24
|
+
return {
|
|
25
|
+
providerID,
|
|
26
|
+
adapterID: 'anthropic',
|
|
27
|
+
label: 'Anthropic',
|
|
28
|
+
shortLabel: 'Anthropic',
|
|
29
|
+
sortOrder: 30,
|
|
30
|
+
status: 'unsupported',
|
|
31
|
+
checkedAt,
|
|
32
|
+
note: 'api key has no public quota endpoint',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
providerID,
|
|
37
|
+
adapterID: 'anthropic',
|
|
38
|
+
label: 'Anthropic',
|
|
39
|
+
shortLabel: 'Anthropic',
|
|
40
|
+
sortOrder: 30,
|
|
41
|
+
status: 'unsupported',
|
|
42
|
+
checkedAt,
|
|
43
|
+
note: 'oauth quota endpoint is not publicly documented',
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { isRecord, swallow } from '../../helpers.js';
|
|
2
|
+
import { asNumber, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from '../common.js';
|
|
3
|
+
async function fetchCopilotQuota(ctx) {
|
|
4
|
+
const checkedAt = Date.now();
|
|
5
|
+
const base = {
|
|
6
|
+
providerID: ctx.providerID,
|
|
7
|
+
adapterID: 'github-copilot',
|
|
8
|
+
label: 'GitHub Copilot',
|
|
9
|
+
shortLabel: 'Copilot',
|
|
10
|
+
sortOrder: 20,
|
|
11
|
+
};
|
|
12
|
+
if (!ctx.auth) {
|
|
13
|
+
return {
|
|
14
|
+
...base,
|
|
15
|
+
status: 'unavailable',
|
|
16
|
+
checkedAt,
|
|
17
|
+
note: 'auth not found',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (ctx.auth.type !== 'oauth') {
|
|
21
|
+
return {
|
|
22
|
+
...base,
|
|
23
|
+
status: 'unsupported',
|
|
24
|
+
checkedAt,
|
|
25
|
+
note: 'oauth token required',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (typeof ctx.auth.access !== 'string' || !ctx.auth.access) {
|
|
29
|
+
return {
|
|
30
|
+
...base,
|
|
31
|
+
status: 'unavailable',
|
|
32
|
+
checkedAt,
|
|
33
|
+
note: 'missing oauth access token',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const response = await fetchWithTimeout('https://api.github.com/copilot_internal/user', {
|
|
37
|
+
headers: {
|
|
38
|
+
Accept: 'application/json',
|
|
39
|
+
Authorization: `token ${ctx.auth.access}`,
|
|
40
|
+
'User-Agent': 'GitHubCopilotChat/0.35.0',
|
|
41
|
+
'Editor-Version': 'vscode/1.107.0',
|
|
42
|
+
'Editor-Plugin-Version': 'copilot-chat/0.35.0',
|
|
43
|
+
'Copilot-Integration-Id': 'vscode-chat',
|
|
44
|
+
},
|
|
45
|
+
}, ctx.config.quota.requestTimeoutMs).catch(swallow('fetchCopilotQuota'));
|
|
46
|
+
if (!response) {
|
|
47
|
+
return {
|
|
48
|
+
...base,
|
|
49
|
+
status: 'error',
|
|
50
|
+
checkedAt,
|
|
51
|
+
note: 'network request failed',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
return {
|
|
56
|
+
...base,
|
|
57
|
+
status: 'error',
|
|
58
|
+
checkedAt,
|
|
59
|
+
note: `http ${response.status}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const payload = await response.json().catch(swallow('fetchCopilotQuota:json'));
|
|
63
|
+
if (!isRecord(payload)) {
|
|
64
|
+
return {
|
|
65
|
+
...base,
|
|
66
|
+
status: 'error',
|
|
67
|
+
checkedAt,
|
|
68
|
+
note: 'invalid response',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const snapshots = isRecord(payload.quota_snapshots)
|
|
72
|
+
? payload.quota_snapshots
|
|
73
|
+
: {};
|
|
74
|
+
const premium = isRecord(snapshots.premium_interactions)
|
|
75
|
+
? snapshots.premium_interactions
|
|
76
|
+
: {};
|
|
77
|
+
const remainingPercent = normalizePercent(premium.percent_remaining) ??
|
|
78
|
+
(() => {
|
|
79
|
+
const entitlement = asNumber(premium.entitlement);
|
|
80
|
+
const remaining = asNumber(premium.remaining);
|
|
81
|
+
if (entitlement === undefined ||
|
|
82
|
+
remaining === undefined ||
|
|
83
|
+
entitlement <= 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return normalizePercent(remaining / entitlement);
|
|
87
|
+
})();
|
|
88
|
+
const resetAt = toIso(payload.quota_reset_date) ?? toIso(premium.quota_reset_date_utc);
|
|
89
|
+
const windows = remainingPercent === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: [
|
|
92
|
+
{
|
|
93
|
+
label: 'Monthly',
|
|
94
|
+
remainingPercent,
|
|
95
|
+
resetAt,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
return {
|
|
99
|
+
...base,
|
|
100
|
+
status: remainingPercent === undefined ? 'error' : 'ok',
|
|
101
|
+
checkedAt,
|
|
102
|
+
remainingPercent,
|
|
103
|
+
resetAt,
|
|
104
|
+
note: remainingPercent === undefined ? 'missing quota fields' : undefined,
|
|
105
|
+
windows,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export const copilotAdapter = {
|
|
109
|
+
id: 'github-copilot',
|
|
110
|
+
label: 'GitHub Copilot',
|
|
111
|
+
shortLabel: 'Copilot',
|
|
112
|
+
sortOrder: 20,
|
|
113
|
+
normalizeID: (providerID) => providerID.startsWith('github-copilot') ? 'github-copilot' : undefined,
|
|
114
|
+
matchScore: ({ providerID }) => providerID.startsWith('github-copilot') ? 80 : 0,
|
|
115
|
+
isEnabled: (config) => configuredProviderEnabled(config.quota, 'github-copilot', config.quota.includeCopilot),
|
|
116
|
+
fetch: fetchCopilotQuota,
|
|
117
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { debug, debugError, isRecord, swallow } from '../../helpers.js';
|
|
2
|
+
import { OPENAI_OAUTH_CLIENT_ID, configuredProviderEnabled, fetchWithTimeout, normalizePercent, parseRateLimitWindow, toIso, } from '../common.js';
|
|
3
|
+
async function fetchOpenAIQuota(ctx) {
|
|
4
|
+
const checkedAt = Date.now();
|
|
5
|
+
const base = {
|
|
6
|
+
providerID: ctx.providerID,
|
|
7
|
+
adapterID: 'openai',
|
|
8
|
+
label: 'OpenAI Codex',
|
|
9
|
+
shortLabel: 'OpenAI',
|
|
10
|
+
sortOrder: 10,
|
|
11
|
+
};
|
|
12
|
+
if (!ctx.auth) {
|
|
13
|
+
return {
|
|
14
|
+
...base,
|
|
15
|
+
status: 'unavailable',
|
|
16
|
+
checkedAt,
|
|
17
|
+
note: 'auth not found',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (ctx.auth.type !== 'oauth') {
|
|
21
|
+
return {
|
|
22
|
+
...base,
|
|
23
|
+
status: 'unsupported',
|
|
24
|
+
checkedAt,
|
|
25
|
+
note: 'api key auth has no quota endpoint',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (typeof ctx.auth.access !== 'string' || !ctx.auth.access) {
|
|
29
|
+
return {
|
|
30
|
+
...base,
|
|
31
|
+
status: 'unavailable',
|
|
32
|
+
checkedAt,
|
|
33
|
+
note: 'missing oauth access token',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
let access = ctx.auth.access;
|
|
37
|
+
let refreshWarning;
|
|
38
|
+
if (ctx.config.quota.refreshAccessToken &&
|
|
39
|
+
ctx.auth.expires &&
|
|
40
|
+
typeof ctx.auth.refresh === 'string' &&
|
|
41
|
+
ctx.auth.refresh &&
|
|
42
|
+
ctx.auth.expires <= Date.now() + 60_000) {
|
|
43
|
+
const refreshed = await fetchWithTimeout('https://auth.openai.com/oauth/token', {
|
|
44
|
+
method: 'POST',
|
|
45
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
46
|
+
body: new URLSearchParams({
|
|
47
|
+
grant_type: 'refresh_token',
|
|
48
|
+
refresh_token: ctx.auth.refresh,
|
|
49
|
+
client_id: OPENAI_OAUTH_CLIENT_ID,
|
|
50
|
+
}).toString(),
|
|
51
|
+
}, ctx.config.quota.requestTimeoutMs).catch(swallow('fetchOpenAIQuota:refresh'));
|
|
52
|
+
if (refreshed?.ok) {
|
|
53
|
+
const payload = await refreshed
|
|
54
|
+
.json()
|
|
55
|
+
.catch(swallow('fetchOpenAIQuota:refreshJson'));
|
|
56
|
+
if (isRecord(payload) && typeof payload.access_token === 'string') {
|
|
57
|
+
access = payload.access_token;
|
|
58
|
+
ctx.auth.access = payload.access_token;
|
|
59
|
+
ctx.auth.refresh =
|
|
60
|
+
typeof payload.refresh_token === 'string'
|
|
61
|
+
? payload.refresh_token
|
|
62
|
+
: ctx.auth.refresh;
|
|
63
|
+
ctx.auth.expires =
|
|
64
|
+
Date.now() +
|
|
65
|
+
(typeof payload.expires_in === 'number' ? payload.expires_in : 3600) *
|
|
66
|
+
1000;
|
|
67
|
+
if (ctx.updateAuth && ctx.auth.refresh && ctx.auth.expires) {
|
|
68
|
+
try {
|
|
69
|
+
await ctx.updateAuth('openai', {
|
|
70
|
+
type: 'oauth',
|
|
71
|
+
access: ctx.auth.access,
|
|
72
|
+
refresh: ctx.auth.refresh,
|
|
73
|
+
expires: ctx.auth.expires,
|
|
74
|
+
accountId: ctx.auth.accountId,
|
|
75
|
+
});
|
|
76
|
+
debug('openai oauth token refreshed and persisted');
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
debugError('updateAuth:openai', error);
|
|
80
|
+
refreshWarning =
|
|
81
|
+
'token refreshed but failed to persist; using in-memory token';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const headers = new Headers({
|
|
88
|
+
Authorization: `Bearer ${access}`,
|
|
89
|
+
Accept: 'application/json',
|
|
90
|
+
'User-Agent': 'opencode-quota-sidebar',
|
|
91
|
+
});
|
|
92
|
+
if (typeof ctx.auth.accountId === 'string' && ctx.auth.accountId) {
|
|
93
|
+
headers.set('ChatGPT-Account-Id', ctx.auth.accountId);
|
|
94
|
+
}
|
|
95
|
+
const response = await fetchWithTimeout('https://chatgpt.com/backend-api/wham/usage', { headers }, ctx.config.quota.requestTimeoutMs).catch(swallow('fetchOpenAIQuota:usage'));
|
|
96
|
+
if (!response) {
|
|
97
|
+
return {
|
|
98
|
+
...base,
|
|
99
|
+
status: 'error',
|
|
100
|
+
checkedAt,
|
|
101
|
+
note: 'network request failed',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
return {
|
|
106
|
+
...base,
|
|
107
|
+
status: 'error',
|
|
108
|
+
checkedAt,
|
|
109
|
+
note: `http ${response.status}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const payload = await response.json().catch(swallow('fetchOpenAIQuota:json'));
|
|
113
|
+
if (!isRecord(payload)) {
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
status: 'error',
|
|
117
|
+
checkedAt,
|
|
118
|
+
note: 'invalid response',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const rateLimit = isRecord(payload.rate_limit) ? payload.rate_limit : {};
|
|
122
|
+
const primary = isRecord(rateLimit.primary_window)
|
|
123
|
+
? rateLimit.primary_window
|
|
124
|
+
: {};
|
|
125
|
+
const usedPercent = normalizePercent(primary.used_percent);
|
|
126
|
+
const remainingPercent = normalizePercent(primary.remaining_percent) ??
|
|
127
|
+
(usedPercent === undefined ? undefined : 100 - usedPercent);
|
|
128
|
+
const resetAt = toIso(primary.reset_at ?? rateLimit.reset_at);
|
|
129
|
+
const windows = [];
|
|
130
|
+
if (remainingPercent !== undefined) {
|
|
131
|
+
const primaryWin = parseRateLimitWindow(primary, '');
|
|
132
|
+
if (primaryWin)
|
|
133
|
+
windows.push(primaryWin);
|
|
134
|
+
}
|
|
135
|
+
if (isRecord(rateLimit.secondary_window)) {
|
|
136
|
+
const secondaryWin = parseRateLimitWindow(rateLimit.secondary_window, 'Weekly');
|
|
137
|
+
if (secondaryWin)
|
|
138
|
+
windows.push(secondaryWin);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
...base,
|
|
142
|
+
status: remainingPercent === undefined ? 'error' : 'ok',
|
|
143
|
+
checkedAt,
|
|
144
|
+
usedPercent,
|
|
145
|
+
remainingPercent,
|
|
146
|
+
resetAt,
|
|
147
|
+
note: remainingPercent === undefined ? 'missing quota fields' : refreshWarning,
|
|
148
|
+
windows: windows.length > 0 ? windows : undefined,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export const openaiAdapter = {
|
|
152
|
+
id: 'openai',
|
|
153
|
+
label: 'OpenAI Codex',
|
|
154
|
+
shortLabel: 'OpenAI',
|
|
155
|
+
sortOrder: 10,
|
|
156
|
+
matchScore: ({ providerID }) => (providerID === 'openai' ? 80 : 0),
|
|
157
|
+
isEnabled: (config) => configuredProviderEnabled(config.quota, 'openai', config.quota.includeOpenAI),
|
|
158
|
+
fetch: fetchOpenAIQuota,
|
|
159
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { anthropicAdapter } from './core/anthropic.js';
|
|
2
|
+
import { copilotAdapter } from './core/copilot.js';
|
|
3
|
+
import { openaiAdapter } from './core/openai.js';
|
|
4
|
+
import { QuotaProviderRegistry } from './registry.js';
|
|
5
|
+
import { rightCodeAdapter } from './third_party/rightcode.js';
|
|
6
|
+
export declare function createDefaultProviderRegistry(): QuotaProviderRegistry;
|
|
7
|
+
export { anthropicAdapter, copilotAdapter, openaiAdapter, rightCodeAdapter, QuotaProviderRegistry, };
|
|
8
|
+
export type { AuthUpdate, AuthValue, ProviderResolveContext, QuotaFetchContext, QuotaProviderAdapter, RefreshedOAuthAuth, } from './types.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { anthropicAdapter } from './core/anthropic.js';
|
|
2
|
+
import { copilotAdapter } from './core/copilot.js';
|
|
3
|
+
import { openaiAdapter } from './core/openai.js';
|
|
4
|
+
import { QuotaProviderRegistry } from './registry.js';
|
|
5
|
+
import { rightCodeAdapter } from './third_party/rightcode.js';
|
|
6
|
+
export function createDefaultProviderRegistry() {
|
|
7
|
+
const registry = new QuotaProviderRegistry();
|
|
8
|
+
registry.register(rightCodeAdapter);
|
|
9
|
+
registry.register(openaiAdapter);
|
|
10
|
+
registry.register(copilotAdapter);
|
|
11
|
+
registry.register(anthropicAdapter);
|
|
12
|
+
return registry;
|
|
13
|
+
}
|
|
14
|
+
export { anthropicAdapter, copilotAdapter, openaiAdapter, rightCodeAdapter, QuotaProviderRegistry, };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ProviderMatch, ProviderResolveContext, QuotaProviderAdapter } from './types.js';
|
|
2
|
+
export declare class QuotaProviderRegistry {
|
|
3
|
+
private adapters;
|
|
4
|
+
register(adapter: QuotaProviderAdapter): void;
|
|
5
|
+
all(): QuotaProviderAdapter[];
|
|
6
|
+
normalizeProviderID(providerID: string): string;
|
|
7
|
+
resolveWithScore(ctx: ProviderResolveContext): ProviderMatch | undefined;
|
|
8
|
+
resolve(ctx: ProviderResolveContext): QuotaProviderAdapter | undefined;
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class QuotaProviderRegistry {
|
|
2
|
+
adapters = [];
|
|
3
|
+
register(adapter) {
|
|
4
|
+
this.adapters.push(adapter);
|
|
5
|
+
}
|
|
6
|
+
all() {
|
|
7
|
+
return [...this.adapters];
|
|
8
|
+
}
|
|
9
|
+
normalizeProviderID(providerID) {
|
|
10
|
+
for (const adapter of this.adapters) {
|
|
11
|
+
if (!adapter.normalizeID)
|
|
12
|
+
continue;
|
|
13
|
+
const normalized = adapter.normalizeID(providerID);
|
|
14
|
+
if (normalized !== undefined)
|
|
15
|
+
return normalized;
|
|
16
|
+
}
|
|
17
|
+
return providerID;
|
|
18
|
+
}
|
|
19
|
+
resolveWithScore(ctx) {
|
|
20
|
+
let best;
|
|
21
|
+
for (const adapter of this.adapters) {
|
|
22
|
+
const score = adapter.matchScore(ctx);
|
|
23
|
+
if (score <= 0)
|
|
24
|
+
continue;
|
|
25
|
+
if (!best || score > best.score) {
|
|
26
|
+
best = { adapter, score };
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (score === best.score && adapter.sortOrder < best.adapter.sortOrder) {
|
|
30
|
+
best = { adapter, score };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return best;
|
|
34
|
+
}
|
|
35
|
+
resolve(ctx) {
|
|
36
|
+
return this.resolveWithScore(ctx)?.adapter;
|
|
37
|
+
}
|
|
38
|
+
}
|