@leo000001/opencode-quota-sidebar 3.0.9 → 4.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +157 -42
  3. package/README.zh-CN.md +157 -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 +302 -41
  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 +101 -8
  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 +18 -8
  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
@@ -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
  };
@@ -1,5 +1,71 @@
1
1
  import { debug, debugError, isRecord, swallow } from '../../helpers.js';
2
- import { OPENAI_OAUTH_CLIENT_ID, configuredProviderEnabled, fetchWithTimeout, normalizePercent, parseRateLimitWindow, toIso, } from '../common.js';
2
+ import { OPENAI_OAUTH_CLIENT_ID, asNumber, configuredProviderEnabled, fetchWithTimeout, toIso, windowLabel, } from '../common.js';
3
+ function decodeJwtPayload(token) {
4
+ try {
5
+ const parts = token.split('.');
6
+ if (parts.length !== 3)
7
+ return undefined;
8
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
9
+ return isRecord(payload) ? payload : undefined;
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ function extractAccountIdFromJwt(token) {
16
+ const payload = decodeJwtPayload(token);
17
+ if (!payload)
18
+ return undefined;
19
+ const authClaim = payload['https://api.openai.com/auth'];
20
+ if (!isRecord(authClaim))
21
+ return undefined;
22
+ const accountID = authClaim.chatgpt_account_id;
23
+ return typeof accountID === 'string' && accountID ? accountID : undefined;
24
+ }
25
+ function normalizeOpenAIQuotaPercent(value) {
26
+ const numeric = asNumber(value);
27
+ if (numeric === undefined || Number.isNaN(numeric))
28
+ return undefined;
29
+ const expanded = numeric > 0 && numeric < 1 ? numeric * 100 : numeric;
30
+ if (expanded < 0)
31
+ return 0;
32
+ if (expanded > 100)
33
+ return 100;
34
+ return expanded;
35
+ }
36
+ function windowResetAt(win, fallback) {
37
+ const absolute = toIso(win.reset_at ?? fallback?.reset_at);
38
+ if (absolute)
39
+ return absolute;
40
+ const resetAfterSeconds = asNumber(win.reset_after_seconds) ?? asNumber(fallback?.reset_after_seconds);
41
+ if (resetAfterSeconds === undefined || resetAfterSeconds < 0)
42
+ return undefined;
43
+ return new Date(Date.now() + resetAfterSeconds * 1000).toISOString();
44
+ }
45
+ function parseOpenAIWindow(win, fallbackLabel, labelPrefix = '') {
46
+ const usedPercent = normalizeOpenAIQuotaPercent(win.used_percent);
47
+ const remainingPercent = normalizeOpenAIQuotaPercent(win.remaining_percent) ??
48
+ (usedPercent === undefined ? undefined : 100 - usedPercent);
49
+ if (remainingPercent === undefined)
50
+ return undefined;
51
+ return {
52
+ label: `${labelPrefix}${windowLabel(win, fallbackLabel)}`.trim(),
53
+ remainingPercent,
54
+ usedPercent,
55
+ resetAt: windowResetAt(win),
56
+ };
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
+ }
3
69
  async function fetchOpenAIQuota(ctx) {
4
70
  const checkedAt = Date.now();
5
71
  const base = {
@@ -85,13 +151,15 @@ async function fetchOpenAIQuota(ctx) {
85
151
  }
86
152
  }
87
153
  }
154
+ const accountId = (typeof ctx.auth.accountId === 'string' && ctx.auth.accountId) ||
155
+ extractAccountIdFromJwt(access);
88
156
  const headers = new Headers({
89
157
  Authorization: `Bearer ${access}`,
90
158
  Accept: 'application/json',
91
159
  'User-Agent': 'opencode-quota-sidebar',
92
160
  });
93
- if (typeof ctx.auth.accountId === 'string' && ctx.auth.accountId) {
94
- headers.set('ChatGPT-Account-Id', ctx.auth.accountId);
161
+ if (accountId) {
162
+ headers.set('ChatGPT-Account-Id', accountId);
95
163
  }
96
164
  const response = await fetchWithTimeout('https://chatgpt.com/backend-api/wham/usage', { headers }, ctx.config.quota.requestTimeoutMs).catch(swallow('fetchOpenAIQuota:usage'));
97
165
  if (!response) {
@@ -123,21 +191,46 @@ async function fetchOpenAIQuota(ctx) {
123
191
  const primary = isRecord(rateLimit.primary_window)
124
192
  ? rateLimit.primary_window
125
193
  : {};
126
- const usedPercent = normalizePercent(primary.used_percent);
127
- const remainingPercent = normalizePercent(primary.remaining_percent) ??
194
+ const usedPercent = normalizeOpenAIQuotaPercent(primary.used_percent);
195
+ const remainingPercent = normalizeOpenAIQuotaPercent(primary.remaining_percent) ??
128
196
  (usedPercent === undefined ? undefined : 100 - usedPercent);
129
- const resetAt = toIso(primary.reset_at ?? rateLimit.reset_at);
197
+ const resetAt = windowResetAt(primary, rateLimit);
130
198
  const windows = [];
131
199
  if (remainingPercent !== undefined) {
132
- const primaryWin = parseRateLimitWindow(primary, '');
200
+ const primaryWin = parseOpenAIWindow(primary, '');
133
201
  if (primaryWin)
134
202
  windows.push(primaryWin);
135
203
  }
136
204
  if (isRecord(rateLimit.secondary_window)) {
137
- const secondaryWin = parseRateLimitWindow(rateLimit.secondary_window, 'Weekly');
205
+ const secondaryWin = parseOpenAIWindow(rateLimit.secondary_window, 'Weekly');
138
206
  if (secondaryWin)
139
207
  windows.push(secondaryWin);
140
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
+ }
141
234
  return {
142
235
  ...base,
143
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;
@@ -1,22 +1,20 @@
1
+ import { isSupportedQuotaSnapshot } from './supported_quota.js';
1
2
  const PROVIDER_SHORT_LABELS = {
2
- openai: "OpenAI",
3
- "kimi-for-coding": "Kimi",
4
- "zhipuai-coding-plan": "Zhipu",
5
- "minimax-cn-coding-plan": "MiniMax",
6
- "github-copilot": "Copilot",
7
- anthropic: "Anthropic",
8
- rightcode: "RC",
9
- xyai: "XYAI",
3
+ openai: 'OpenAI',
4
+ 'kimi-for-coding': 'Kimi',
5
+ 'zhipuai-coding-plan': 'Zhipu',
6
+ 'minimax-cn-coding-plan': 'MiniMax',
7
+ 'github-copilot': 'Copilot',
8
+ anthropic: 'Anthropic',
9
+ rightcode: 'RC',
10
10
  };
11
11
  export function canonicalProviderID(providerID) {
12
- if (providerID.startsWith("github-copilot"))
13
- return "github-copilot";
14
- if (providerID === "zhipuai-coding-plan")
15
- return "zhipuai-coding-plan";
16
- if (providerID === "minimax-cn-coding-plan")
17
- return "minimax-cn-coding-plan";
18
- if (providerID === "xyai-vibe")
19
- return "xyai";
12
+ if (providerID.startsWith('github-copilot'))
13
+ return 'github-copilot';
14
+ if (providerID === 'zhipuai-coding-plan')
15
+ return 'zhipuai-coding-plan';
16
+ if (providerID === 'minimax-cn-coding-plan')
17
+ return 'minimax-cn-coding-plan';
20
18
  return providerID;
21
19
  }
22
20
  export function displayShortLabel(providerID) {
@@ -24,8 +22,8 @@ export function displayShortLabel(providerID) {
24
22
  const direct = PROVIDER_SHORT_LABELS[canonical];
25
23
  if (direct)
26
24
  return direct;
27
- if (canonical.startsWith("rightcode-")) {
28
- return `RC-${canonical.slice("rightcode-".length)}`;
25
+ if (canonical.startsWith('rightcode-')) {
26
+ return `RC-${canonical.slice('rightcode-'.length)}`;
29
27
  }
30
28
  return providerID;
31
29
  }
@@ -40,13 +38,13 @@ export function quotaDisplayLabel(quota) {
40
38
  return displayShortLabel(quota.providerID);
41
39
  }
42
40
  function quotaKey(quota) {
43
- if (quota.adapterID === "rightcode")
41
+ if (quota.adapterID === 'rightcode')
44
42
  return `rightcode:${quota.providerID}`;
45
43
  return `${quota.adapterID || quota.providerID}:${quota.providerID}`;
46
44
  }
47
45
  function quotaScore(quota) {
48
46
  let score = 0;
49
- if (quota.status === "ok")
47
+ if (quota.status === 'ok')
50
48
  score += 10;
51
49
  if (quota.windows && quota.windows.length > 0) {
52
50
  score += 5 + quota.windows.length;
@@ -58,14 +56,15 @@ function quotaScore(quota) {
58
56
  return score;
59
57
  }
60
58
  export function collapseQuotaSnapshots(quotas) {
59
+ const supportedQuotas = quotas.filter((quota) => isSupportedQuotaSnapshot(quota));
61
60
  const grouped = new Map();
62
- const hasRightCodeBase = quotas.some((quota) => quota.adapterID === "rightcode" && quotaDisplayLabel(quota) === "RC");
63
- for (const quota of quotas) {
61
+ const hasRightCodeBase = supportedQuotas.some((quota) => quota.adapterID === 'rightcode' && quotaDisplayLabel(quota) === 'RC');
62
+ for (const quota of supportedQuotas) {
64
63
  // If both RC (balance) and RC-variant (subscription) exist,
65
64
  // treat balance as owned by RC.
66
65
  const normalizedQuota = hasRightCodeBase &&
67
- quota.adapterID === "rightcode" &&
68
- quotaDisplayLabel(quota).startsWith("RC-")
66
+ quota.adapterID === 'rightcode' &&
67
+ quotaDisplayLabel(quota).startsWith('RC-')
69
68
  ? { ...quota, balance: undefined }
70
69
  : quota;
71
70
  const key = quotaKey(normalizedQuota);
@@ -20,4 +20,5 @@ export declare function createQuotaService(deps: {
20
20
  getQuotaSnapshots: (providerIDs: string[], options?: {
21
21
  allowDefault?: boolean;
22
22
  }) => Promise<QuotaSnapshot[]>;
23
+ invalidateForProvider: (providerID: string) => void;
23
24
  };