@astrosheep/pi-quota 0.12.2 → 0.13.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/README.md +78 -12
- package/dist/query/codex.d.ts +2 -2
- package/dist/query/codex.js +1 -1
- package/dist/query/deepseek.d.ts +2 -2
- package/dist/query/deepseek.js +7 -8
- package/dist/query/kimi.d.ts +2 -2
- package/dist/query/kimi.js +1 -1
- package/dist/query/new-api.d.ts +1 -1
- package/dist/query/new-api.js +24 -20
- package/dist/query/opencode-go.d.ts +2 -2
- package/dist/query/opencode-go.js +1 -1
- package/dist/query/registry.d.ts +1 -1
- package/dist/query/registry.js +70 -43
- package/dist/query/sub2api.js +31 -32
- package/dist/query/types.d.ts +22 -19
- package/dist/query/yescode.js +9 -10
- package/examples/custom-provider.ts +3 -1
- package/examples/preview.ts +2 -2
- package/package.json +1 -1
- package/src/bar/balance.ts +10 -48
- package/src/bar/card.ts +36 -47
- package/src/bar/quota.ts +54 -49
- package/src/extension.ts +3 -3
- package/src/query/codex.ts +4 -4
- package/src/query/deepseek.ts +8 -9
- package/src/query/kimi.ts +5 -5
- package/src/query/new-api.ts +24 -19
- package/src/query/opencode-go.ts +4 -4
- package/src/query/registry.ts +72 -47
- package/src/query/sub2api.ts +32 -33
- package/src/query/types.ts +25 -22
- package/src/query/yescode.ts +14 -15
package/src/query/new-api.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { bearer } from './http.ts';
|
|
2
|
-
import { numeric, object } from './parse.ts';
|
|
2
|
+
import { numeric, object, percent } from './parse.ts';
|
|
3
3
|
import { QuotaError } from './types.ts';
|
|
4
4
|
import { safeBaseUrl } from './url.ts';
|
|
5
5
|
import type { QuotaAdapter, QuotaSnapshot } from './types.ts';
|
|
6
6
|
|
|
7
|
-
type Amounts =
|
|
7
|
+
type Amounts = Omit<QuotaSnapshot, 'fetchedAt'>;
|
|
8
8
|
|
|
9
9
|
export interface NewApiOptions {
|
|
10
10
|
quotaPerUnit?: number;
|
|
@@ -43,9 +43,11 @@ export function parseNewApiTokenUsage(payload: unknown, options: NewApiOptions =
|
|
|
43
43
|
if (data.object !== 'token_usage') return null;
|
|
44
44
|
const used = numeric(data.total_used);
|
|
45
45
|
if (used === null || !Number.isSafeInteger(used) || used < 0) throw new QuotaError('schema');
|
|
46
|
+
const spending = [{ id: 'key-lifetime', label: 'API key spend', currency,
|
|
47
|
+
amount: used / quotaPerUnit, period: 'lifetime' as const }];
|
|
46
48
|
if (data.unlimited_quota === true) {
|
|
47
|
-
return {
|
|
48
|
-
|
|
49
|
+
return { balances: [], limits: [{ id: 'key-quota', label: 'Key quota',
|
|
50
|
+
remainingPercent: null, resetAt: null, unlimited: true }], spending };
|
|
49
51
|
}
|
|
50
52
|
const remaining = numeric(data.total_available);
|
|
51
53
|
if (remaining === null || !Number.isSafeInteger(remaining)) throw new QuotaError('schema');
|
|
@@ -54,15 +56,17 @@ export function parseNewApiTokenUsage(payload: unknown, options: NewApiOptions =
|
|
|
54
56
|
|
|
55
57
|
// New-api reports granted = key used + key remaining. Only draw a finite
|
|
56
58
|
// allocation bar when the server actually supplies that total.
|
|
59
|
+
let limit: number | null = null;
|
|
57
60
|
if (data.total_granted !== undefined) {
|
|
58
61
|
const granted = numeric(data.total_granted);
|
|
59
62
|
if (granted === null || !Number.isSafeInteger(granted) || granted < 0
|
|
60
63
|
|| !Number.isSafeInteger(used + remaining) || granted !== used + remaining) throw new QuotaError('schema');
|
|
61
|
-
|
|
62
|
-
used: used / quotaPerUnit, remaining: moneyLeft } };
|
|
64
|
+
limit = granted / quotaPerUnit;
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
// Key quota is not owned account money. Missing total_granted stays unknown.
|
|
67
|
+
return { balances: [], limits: [{ id: 'key-quota', label: 'Key quota', resetAt: null,
|
|
68
|
+
remainingPercent: limit !== null && limit > 0 ? percent(moneyLeft / limit * 100) : null,
|
|
69
|
+
amounts: { unit: currency, limit, used: used / quotaPerUnit, remaining: moneyLeft } }], spending };
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
// GET /api/user/self (UserAuth): the console's own account quota, readable with
|
|
@@ -78,8 +82,9 @@ export function parseNewApiUserSelf(payload: unknown, options: NewApiOptions = {
|
|
|
78
82
|
if (quota === null || usedQuota === null
|
|
79
83
|
|| !Number.isSafeInteger(quota) || !Number.isSafeInteger(usedQuota)
|
|
80
84
|
|| quota < 0 || usedQuota < 0) throw new QuotaError('schema');
|
|
81
|
-
return {
|
|
82
|
-
|
|
85
|
+
return { balances: [{ id: 'account', label: 'Balance', currency, remaining: quota / quotaPerUnit }],
|
|
86
|
+
limits: [], spending: [{ id: 'account-lifetime', label: 'Account spend', currency,
|
|
87
|
+
amount: usedQuota / quotaPerUnit, period: 'lifetime' }] };
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
// Legacy one-api billing pair, still the only option on older deployments.
|
|
@@ -96,10 +101,10 @@ export function parseNewApiBilling(subscription: unknown, usage: unknown, option
|
|
|
96
101
|
const used = totalUsage / 100;
|
|
97
102
|
// This endpoint reports account remaining + lifetime usage as its hard limit.
|
|
98
103
|
// Top-ups change that total, so it is a wallet balance, not a fixed allowance.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
104
|
+
const spending = [{ id: 'billing-lifetime', label: 'Reported spend', currency, amount: used, period: 'lifetime' as const }];
|
|
105
|
+
if (limit >= 1e7) return { balances: [], limits: [{ id: 'billing-quota', label: 'Quota',
|
|
106
|
+
remainingPercent: null, resetAt: null, unlimited: true }], spending };
|
|
107
|
+
return { balances: [{ id: 'account', label: 'Balance', currency, remaining: limit - used }], limits: [], spending };
|
|
103
108
|
}
|
|
104
109
|
|
|
105
110
|
export function createNewApiAdapter(provider: string, options: NewApiOptions = {}): QuotaAdapter {
|
|
@@ -129,7 +134,7 @@ export function createNewApiAdapter(provider: string, options: NewApiOptions = {
|
|
|
129
134
|
if (dashboardUserId !== undefined) headers['New-Api-User'] = String(dashboardUserId);
|
|
130
135
|
try {
|
|
131
136
|
const payload = await context.getJson(`${root}/api/user/self`, headers, context.signal);
|
|
132
|
-
return {
|
|
137
|
+
return { ...parseNewApiUserSelf(payload, settings), fetchedAt: context.now() };
|
|
133
138
|
} catch (error) {
|
|
134
139
|
if (!(error instanceof QuotaError)) throw error;
|
|
135
140
|
}
|
|
@@ -145,7 +150,7 @@ export function createNewApiAdapter(provider: string, options: NewApiOptions = {
|
|
|
145
150
|
context.getJson(`${root}/v1/dashboard/billing/usage?start_date=2020-01-01&end_date=2100-01-01`, headers, context.signal),
|
|
146
151
|
]);
|
|
147
152
|
const billing = parseNewApiBilling(pair[0], pair[1], settings);
|
|
148
|
-
if (billing.
|
|
153
|
+
if (billing.balances.length > 0) return { ...billing, fetchedAt: context.now() };
|
|
149
154
|
billingBalance = billing;
|
|
150
155
|
} catch (error) {
|
|
151
156
|
// 404: deployment has no billing pair. 401: try the key-native endpoint
|
|
@@ -157,15 +162,15 @@ export function createNewApiAdapter(provider: string, options: NewApiOptions = {
|
|
|
157
162
|
try {
|
|
158
163
|
const payload = await context.getJson(`${root}/api/usage/token/`, headers, context.signal);
|
|
159
164
|
const parsed = parseNewApiTokenUsage(payload, settings);
|
|
160
|
-
if (parsed) return {
|
|
165
|
+
if (parsed) return { ...parsed, fetchedAt: context.now() };
|
|
161
166
|
} catch (error) {
|
|
162
167
|
if (billingBalance && error instanceof QuotaError && error.code === 'http') {
|
|
163
|
-
return {
|
|
168
|
+
return { ...billingBalance, fetchedAt: context.now() };
|
|
164
169
|
}
|
|
165
170
|
if (error instanceof QuotaError && error.code === 'auth') throw new QuotaError('account-access');
|
|
166
171
|
throw error;
|
|
167
172
|
}
|
|
168
|
-
if (billingBalance) return {
|
|
173
|
+
if (billingBalance) return { ...billingBalance, fetchedAt: context.now() };
|
|
169
174
|
throw new QuotaError('schema');
|
|
170
175
|
},
|
|
171
176
|
};
|
package/src/query/opencode-go.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bearer, officialAuth } from './http.ts';
|
|
2
2
|
import { numeric, object, percent, timestamp, windowLabel } from './parse.ts';
|
|
3
3
|
import { QuotaError } from './types.ts';
|
|
4
|
-
import type { QuotaAdapter,
|
|
4
|
+
import type { QuotaAdapter, UsageLimit } from './types.ts';
|
|
5
5
|
|
|
6
6
|
// OpenCode Go (opencode.ai Zen Go plan) is a single official deployment, so the
|
|
7
7
|
// window keys are fixed and undecorated: percent is already "used", and there is
|
|
@@ -12,9 +12,9 @@ const WINDOWS = [
|
|
|
12
12
|
{ key: 'monthly', duration: 2592000 }, // 30 days
|
|
13
13
|
] as const;
|
|
14
14
|
|
|
15
|
-
export function parseOpenCodeGo(payload: unknown):
|
|
15
|
+
export function parseOpenCodeGo(payload: unknown): UsageLimit[] {
|
|
16
16
|
const usage = object(object(payload).usage);
|
|
17
|
-
const windows:
|
|
17
|
+
const windows: UsageLimit[] = [];
|
|
18
18
|
for (const { key, duration } of WINDOWS) {
|
|
19
19
|
if (usage[key] === undefined) continue;
|
|
20
20
|
const detail = object(usage[key]);
|
|
@@ -41,7 +41,7 @@ export function createOpenCodeGoAdapter(provider: string): QuotaAdapter {
|
|
|
41
41
|
const data = await context.getJson('https://opencode.ai/zen/go/v1/usage', {
|
|
42
42
|
Authorization: `Bearer ${bearer(auth)}`,
|
|
43
43
|
}, context.signal);
|
|
44
|
-
return {
|
|
44
|
+
return { balances: [], limits: parseOpenCodeGo(data), spending: [], fetchedAt: context.now() };
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
}
|
package/src/query/registry.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { safeLabel } from './parse.ts';
|
|
2
2
|
import { QuotaError } from './types.ts';
|
|
3
|
-
import type {
|
|
3
|
+
import type { LimitAmounts, QuotaAdapter, QuotaSnapshot } from './types.ts';
|
|
4
4
|
|
|
5
5
|
export class AdapterRegistry {
|
|
6
6
|
private readonly adapters = new Map<string, QuotaAdapter>();
|
|
@@ -25,58 +25,83 @@ export class AdapterRegistry {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Validate even custom adapter output before it reaches
|
|
29
|
-
export function validateSnapshot(snapshot:
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const data = snapshot.spend;
|
|
47
|
-
if (!data || typeof data.currency !== 'string' || !/^[A-Z]{3}$/.test(data.currency)
|
|
48
|
-
|| (data.today === undefined && data.lifetime === undefined)
|
|
49
|
-
|| (data.today !== undefined && (!Number.isFinite(data.today) || data.today < 0))
|
|
50
|
-
|| (data.lifetime !== undefined && (!Number.isFinite(data.lifetime) || data.lifetime < 0))) {
|
|
28
|
+
// Validate even custom/exec adapter output before it reaches a consumer.
|
|
29
|
+
export function validateSnapshot(snapshot: unknown): QuotaSnapshot {
|
|
30
|
+
const data = record(snapshot);
|
|
31
|
+
if (!Number.isFinite(data.fetchedAt)
|
|
32
|
+
|| ['windows', 'balance', 'allowance', 'spend'].some(key => key in data)) throw new QuotaError('schema');
|
|
33
|
+
const balances = entries(data.balances).map(entry => {
|
|
34
|
+
if (!isCurrency(entry.currency) || !Number.isFinite(entry.remaining)
|
|
35
|
+
|| entry.unlimited !== undefined) throw new QuotaError('schema');
|
|
36
|
+
return { ...identity(entry), currency: entry.currency, remaining: entry.remaining as number };
|
|
37
|
+
});
|
|
38
|
+
const limits = entries(data.limits).map(entry => {
|
|
39
|
+
if ((entry.scope !== undefined && typeof entry.scope !== 'string')
|
|
40
|
+
|| (entry.durationSeconds !== undefined && (!Number.isFinite(entry.durationSeconds) || (entry.durationSeconds as number) <= 0))
|
|
41
|
+
|| (entry.remainingPercent !== null && (!Number.isFinite(entry.remainingPercent)
|
|
42
|
+
|| (entry.remainingPercent as number) < 0 || (entry.remainingPercent as number) > 100))
|
|
43
|
+
|| (entry.resetAt !== null && (!Number.isFinite(entry.resetAt) || (entry.resetAt as number) <= 0))
|
|
44
|
+
|| (entry.unlimited !== undefined && entry.unlimited !== true)
|
|
45
|
+
|| (entry.unlimited === true && (entry.amounts !== undefined || entry.remainingPercent !== null))) {
|
|
51
46
|
throw new QuotaError('schema');
|
|
52
47
|
}
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
return { ...identity(entry), remainingPercent: entry.remainingPercent as number | null,
|
|
49
|
+
resetAt: entry.resetAt as number | null,
|
|
50
|
+
...(entry.scope !== undefined ? { scope: label(entry.scope) } : {}),
|
|
51
|
+
...(entry.durationSeconds !== undefined ? { durationSeconds: entry.durationSeconds as number } : {}),
|
|
52
|
+
...(entry.amounts !== undefined ? { amounts: validateAmounts(entry.amounts) } : {}),
|
|
53
|
+
...(entry.unlimited === true ? { unlimited: true as const } : {}) };
|
|
54
|
+
});
|
|
55
|
+
const spending = entries(data.spending).map(entry => {
|
|
56
|
+
if (!isCurrency(entry.currency) || !Number.isFinite(entry.amount) || (entry.amount as number) < 0
|
|
57
|
+
|| (entry.period !== 'today' && entry.period !== 'lifetime')) throw new QuotaError('schema');
|
|
58
|
+
return { ...identity(entry), currency: entry.currency, amount: entry.amount as number, period: entry.period as 'today' | 'lifetime' };
|
|
59
|
+
});
|
|
60
|
+
if (balances.length + limits.length + spending.length === 0) throw new QuotaError('schema');
|
|
61
|
+
return { balances, limits, spending, fetchedAt: data.fetchedAt as number };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function record(value: unknown): Record<string, unknown> {
|
|
65
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new QuotaError('schema');
|
|
66
|
+
return value as Record<string, unknown>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function label(value: unknown): string {
|
|
70
|
+
if (typeof value !== 'string') throw new QuotaError('schema');
|
|
71
|
+
const clean = safeLabel(value).trim();
|
|
72
|
+
if (!clean) throw new QuotaError('schema');
|
|
73
|
+
return clean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function identity(data: Record<string, unknown>): { id: string; label: string } {
|
|
77
|
+
// IDs are opaque and preserved, never derived from duration, scope or label.
|
|
78
|
+
if (typeof data.id !== 'string' || !data.id || data.id !== label(data.id) || data.id.length > 64) {
|
|
79
|
+
throw new QuotaError('schema');
|
|
55
80
|
}
|
|
81
|
+
return { id: data.id, label: label(data.label) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function entries(value: unknown): Record<string, unknown>[] {
|
|
85
|
+
if (!Array.isArray(value) || value.length > 64) throw new QuotaError('schema');
|
|
56
86
|
const ids = new Set<string>();
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|| window.remainingPercent < 0 || window.remainingPercent > 100))
|
|
64
|
-
|| (window.resetAt !== null && (!Number.isFinite(window.resetAt) || window.resetAt <= 0))) {
|
|
65
|
-
throw new QuotaError('schema');
|
|
66
|
-
}
|
|
67
|
-
ids.add(window.id);
|
|
68
|
-
return { ...window, label: safeLabel(window.label),
|
|
69
|
-
...(window.amounts !== undefined ? { amounts: validateAllowance(window.amounts) } : {}),
|
|
70
|
-
...(window.scope !== undefined ? { scope: safeLabel(window.scope) } : {}) };
|
|
87
|
+
return value.map(item => {
|
|
88
|
+
const data = record(item);
|
|
89
|
+
const { id } = identity(data);
|
|
90
|
+
if (ids.has(id)) throw new QuotaError('schema');
|
|
91
|
+
ids.add(id);
|
|
92
|
+
return data;
|
|
71
93
|
});
|
|
72
|
-
return { fetchedAt: snapshot.fetchedAt, windows, ...(balance ? { balance } : {}),
|
|
73
|
-
...(allowance ? { allowance } : {}), ...(spend ? { spend } : {}) };
|
|
74
94
|
}
|
|
75
95
|
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
function isCurrency(value: unknown): value is string {
|
|
97
|
+
return typeof value === 'string' && /^[A-Z]{3}$/.test(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function validateAmounts(value: unknown): LimitAmounts {
|
|
101
|
+
const data = record(value);
|
|
102
|
+
if ((data.limit !== null && (!Number.isFinite(data.limit) || (data.limit as number) < 0))
|
|
103
|
+
|| !Number.isFinite(data.used) || (data.used as number) < 0
|
|
80
104
|
|| !Number.isFinite(data.remaining)) throw new QuotaError('schema');
|
|
81
|
-
return {
|
|
105
|
+
return { unit: label(data.unit), limit: data.limit as number | null,
|
|
106
|
+
used: data.used as number, remaining: data.remaining as number };
|
|
82
107
|
}
|
package/src/query/sub2api.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { bearer } from './http.ts';
|
|
|
2
2
|
import { numeric, object, percent, timestamp } from './parse.ts';
|
|
3
3
|
import { QuotaError } from './types.ts';
|
|
4
4
|
import { safeBaseUrl } from './url.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type { Balance, LimitAmounts, QuotaAdapter, QuotaSnapshot, UsageLimit, ProviderAuth, Spending } from './types.ts';
|
|
6
6
|
|
|
7
7
|
export function sub2ApiUsageUrl(baseUrl: string | undefined): string {
|
|
8
8
|
const url = safeBaseUrl(baseUrl);
|
|
@@ -23,23 +23,21 @@ function amount(value: unknown): number {
|
|
|
23
23
|
return result;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function allowance(data: Record<string, unknown>, unit: string):
|
|
26
|
+
function allowance(data: Record<string, unknown>, unit: string): LimitAmounts {
|
|
27
27
|
const limit = amount(data.limit);
|
|
28
28
|
if (limit === 0) throw new QuotaError('schema');
|
|
29
29
|
const used = amount(data.used);
|
|
30
30
|
const remaining = amount(data.remaining);
|
|
31
|
-
return {
|
|
31
|
+
return { unit, limit, used, remaining };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function spend(data: Record<string, unknown>,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return { currency: unit, ...(todayCost !== null ? { today: todayCost } : {}),
|
|
42
|
-
...(lifetimeCost !== null ? { lifetime: lifetimeCost } : {}) };
|
|
34
|
+
function spend(data: Record<string, unknown>, currency: string): Spending[] {
|
|
35
|
+
return [['today', data.today], ['lifetime', data.total]].flatMap(([period, raw]) => {
|
|
36
|
+
const cost = object(raw).actual_cost;
|
|
37
|
+
if (cost === undefined) return [];
|
|
38
|
+
return [{ id: `key-${period}`, label: 'API key spend', currency, amount: amount(cost),
|
|
39
|
+
period: period as 'today' | 'lifetime' }];
|
|
40
|
+
});
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
function percentFromAmounts(remaining: number, limit: number): number {
|
|
@@ -48,16 +46,16 @@ function percentFromAmounts(remaining: number, limit: number): number {
|
|
|
48
46
|
|
|
49
47
|
const RATE_WINDOWS: Record<string, number> = { '5h': 18000, '1d': 86400, '7d': 604800 };
|
|
50
48
|
|
|
51
|
-
function rateWindow(data: Record<string, unknown
|
|
49
|
+
function rateWindow(data: Record<string, unknown>): UsageLimit {
|
|
52
50
|
const label = data.window;
|
|
53
51
|
if (typeof label !== 'string' || !Object.hasOwn(RATE_WINDOWS, label)) throw new QuotaError('schema');
|
|
54
|
-
const limits = allowance(data,
|
|
52
|
+
const limits = allowance(data, 'USD');
|
|
55
53
|
return { id: `rate-${label}`, label, durationSeconds: RATE_WINDOWS[label],
|
|
56
|
-
remainingPercent: percentFromAmounts(limits.remaining, limits.limit),
|
|
54
|
+
remainingPercent: percentFromAmounts(limits.remaining, limits.limit!),
|
|
57
55
|
resetAt: timestamp(data.reset_at), amounts: limits };
|
|
58
56
|
}
|
|
59
57
|
|
|
60
|
-
function subscriptionWindows(data: Record<string, unknown
|
|
58
|
+
function subscriptionWindows(data: Record<string, unknown>): UsageLimit[] {
|
|
61
59
|
const specs = [
|
|
62
60
|
['daily', '1d', 'daily_usage_usd', 'daily_limit_usd', 86400],
|
|
63
61
|
['weekly', '1w', 'weekly_usage_usd', 'weekly_limit_usd', 604800],
|
|
@@ -70,10 +68,9 @@ function subscriptionWindows(data: Record<string, unknown>, unit: string): Quota
|
|
|
70
68
|
if (limit === 0) return []; // No cap for this period; no percentage can be computed.
|
|
71
69
|
const used = numeric(data[usedKey]);
|
|
72
70
|
if (used === null || used < 0) throw new QuotaError('schema');
|
|
73
|
-
const amounts:
|
|
74
|
-
const start = id === 'weekly' ? timestamp(data.weekly_window_start) : null;
|
|
71
|
+
const amounts: LimitAmounts = { unit: 'USD', limit, used, remaining: Math.max(0, limit - used) };
|
|
75
72
|
return [{ id, label, durationSeconds, remainingPercent: percentFromAmounts(amounts.remaining, limit),
|
|
76
|
-
resetAt:
|
|
73
|
+
resetAt: null, amounts }];
|
|
77
74
|
});
|
|
78
75
|
}
|
|
79
76
|
|
|
@@ -86,27 +83,29 @@ export function parseSub2ApiUsage(payload: unknown): Omit<QuotaSnapshot, 'fetche
|
|
|
86
83
|
// Sub2API rate limits are always USD; a fixed quota carries its own unit.
|
|
87
84
|
const unit = currency(body.unit ?? (body.mode === 'quota_limited'
|
|
88
85
|
? object(body.quota).unit ?? 'USD' : undefined));
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
let finiteQuota: QuotaAllowance | undefined;
|
|
86
|
+
const limits: UsageLimit[] = [];
|
|
87
|
+
const balances: Balance[] = [];
|
|
92
88
|
if (body.mode === 'quota_limited') {
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
if (body.quota !== undefined) {
|
|
90
|
+
const amounts = allowance(object(body.quota), unit);
|
|
91
|
+
limits.push({ id: 'key-quota', label: 'Key quota', resetAt: null,
|
|
92
|
+
remainingPercent: percentFromAmounts(amounts.remaining, amounts.limit!), amounts });
|
|
93
|
+
}
|
|
95
94
|
const rates = body.rate_limits;
|
|
96
95
|
if (rates !== undefined) {
|
|
97
96
|
if (!Array.isArray(rates)) throw new QuotaError('schema');
|
|
98
|
-
rates.forEach(entry =>
|
|
97
|
+
rates.forEach(entry => limits.push(rateWindow(object(entry))));
|
|
99
98
|
}
|
|
100
99
|
} else if (body.subscription !== undefined) {
|
|
101
|
-
|
|
102
|
-
} else {
|
|
103
|
-
const remaining =
|
|
104
|
-
|
|
100
|
+
limits.push(...subscriptionWindows(object(body.subscription)));
|
|
101
|
+
} else if (body.remaining !== undefined || body.balance !== undefined) {
|
|
102
|
+
const remaining = numeric(body.remaining ?? body.balance);
|
|
103
|
+
if (remaining === null) throw new QuotaError('schema');
|
|
104
|
+
balances.push({ id: 'wallet', label: 'Balance', currency: unit, remaining });
|
|
105
105
|
}
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
108
|
-
return {
|
|
109
|
-
...(usage ? { spend: usage } : {}) };
|
|
106
|
+
const spending = spend(object(body.usage), unit);
|
|
107
|
+
if (limits.length + balances.length + spending.length === 0) throw new QuotaError('schema');
|
|
108
|
+
return { limits, balances, spending };
|
|
110
109
|
}
|
|
111
110
|
|
|
112
111
|
export function createSub2ApiAdapter(provider: string): QuotaAdapter {
|
package/src/query/types.ts
CHANGED
|
@@ -1,38 +1,41 @@
|
|
|
1
1
|
// Query-layer contracts: no Pi, terminal, or rendering dependencies.
|
|
2
|
-
export interface
|
|
2
|
+
export interface Balance {
|
|
3
3
|
id: string;
|
|
4
4
|
label: string;
|
|
5
|
-
remainingPercent: number | null; // null is unknown, never silently 0 or 100
|
|
6
|
-
resetAt: number | null; // epoch milliseconds
|
|
7
|
-
durationSeconds: number;
|
|
8
|
-
scope?: string; // e.g. a model-specific Codex quota domain
|
|
9
|
-
amounts?: QuotaAllowance; // exact periodic amounts when exposed by the API
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface AccountBalance {
|
|
13
5
|
currency: string; // ISO-style currency code, e.g. USD
|
|
14
|
-
remaining: number; // may be negative (debt); not a
|
|
15
|
-
unlimited?: boolean; // provider granted no finite cap; ignore remaining
|
|
6
|
+
remaining: number; // finite; may be negative (debt); owned money, not a quota
|
|
16
7
|
}
|
|
17
8
|
|
|
18
|
-
export interface
|
|
19
|
-
|
|
20
|
-
limit: number;
|
|
9
|
+
export interface LimitAmounts {
|
|
10
|
+
unit: string; // currency code or a count unit, e.g. USD, CNY, tokens, requests
|
|
11
|
+
limit: number | null; // null when the API supplies amounts but no total cap
|
|
21
12
|
used: number;
|
|
22
|
-
remaining: number;
|
|
13
|
+
remaining: number; // may be negative when overdrawn
|
|
23
14
|
}
|
|
24
15
|
|
|
25
|
-
export interface
|
|
16
|
+
export interface UsageLimit {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
scope?: string; // e.g. a model-specific Codex quota domain
|
|
20
|
+
remainingPercent: number | null; // remaining, not used; null is unknown
|
|
21
|
+
resetAt: number | null; // epoch milliseconds; null is unknown/not supplied
|
|
22
|
+
durationSeconds?: number; // absence says nothing about whether it renews
|
|
23
|
+
amounts?: LimitAmounts; // independent of rounded/provider-specific percentages
|
|
24
|
+
unlimited?: true; // no finite cap; no amounts or fabricated percentage
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Spending {
|
|
28
|
+
id: string;
|
|
29
|
+
label: string;
|
|
26
30
|
currency: string;
|
|
27
|
-
|
|
28
|
-
lifetime
|
|
31
|
+
amount: number;
|
|
32
|
+
period: 'today' | 'lifetime'; // today uses the provider's day, not an inferred timezone
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
export interface QuotaSnapshot {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
spend?: SpendSummary;
|
|
36
|
+
balances: Balance[];
|
|
37
|
+
limits: UsageLimit[];
|
|
38
|
+
spending: Spending[];
|
|
36
39
|
fetchedAt: number;
|
|
37
40
|
}
|
|
38
41
|
|
package/src/query/yescode.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { officialAuth } from './http.ts';
|
|
2
2
|
import { numeric, object, percent, timestamp, windowLabel } from './parse.ts';
|
|
3
3
|
import { QuotaError } from './types.ts';
|
|
4
|
-
import type { PostJsonResult, QuotaAdapter, QuotaSnapshot,
|
|
4
|
+
import type { PostJsonResult, QuotaAdapter, QuotaSnapshot, UsageLimit } from './types.ts';
|
|
5
5
|
|
|
6
6
|
export const YESCODE_ORIGIN = 'https://co.yes.vg';
|
|
7
7
|
|
|
@@ -70,7 +70,7 @@ function nonNegative(value: unknown): number | undefined {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function limitWindow(id: string, label: string, durationSeconds: number,
|
|
73
|
-
limitRaw: unknown, usedRaw: unknown, resetAt: number | null):
|
|
73
|
+
limitRaw: unknown, usedRaw: unknown, resetAt: number | null): UsageLimit[] {
|
|
74
74
|
const used = nonNegative(usedRaw); // validate present-but-corrupt spends even without a cap
|
|
75
75
|
const limit = nonNegative(limitRaw);
|
|
76
76
|
if (limit === undefined || limit === 0) return []; // no configured cap: nothing to draw
|
|
@@ -79,17 +79,17 @@ function limitWindow(id: string, label: string, durationSeconds: number,
|
|
|
79
79
|
}
|
|
80
80
|
const remaining = Math.max(0, limit - used);
|
|
81
81
|
return [{ id, label, durationSeconds, remainingPercent: percent(remaining / limit * 100), resetAt,
|
|
82
|
-
amounts: {
|
|
82
|
+
amounts: { unit: 'USD', limit, used, remaining } }];
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// GET /api/v1/user/balance (optional; some accounts may lack it) merged with
|
|
86
86
|
// GET /api/v1/auth/profile (required). The live response carries a generic
|
|
87
87
|
// spending_limits array (scope/spent/limit/remaining/resets_at/window_seconds)
|
|
88
88
|
// plus top-level weekly_* and monthly_* fields; profile is the last fallback.
|
|
89
|
-
function spendingLimitWindows(balanceData: Record<string, unknown>):
|
|
89
|
+
function spendingLimitWindows(balanceData: Record<string, unknown>): UsageLimit[] | null {
|
|
90
90
|
if (balanceData.spending_limits === undefined) return null;
|
|
91
91
|
if (!Array.isArray(balanceData.spending_limits)) throw new QuotaError('schema');
|
|
92
|
-
const windows = balanceData.spending_limits.flatMap((entry):
|
|
92
|
+
const windows = balanceData.spending_limits.flatMap((entry): UsageLimit[] => {
|
|
93
93
|
const item = object(entry);
|
|
94
94
|
const limit = nonNegative(item.limit);
|
|
95
95
|
const spent = nonNegative(item.spent);
|
|
@@ -108,10 +108,10 @@ function spendingLimitWindows(balanceData: Record<string, unknown>): QuotaWindow
|
|
|
108
108
|
const left = remaining ?? Math.max(0, limit - spent);
|
|
109
109
|
return [{ id: `spending:${item.key}`, label: windowLabel(windowSeconds), durationSeconds: windowSeconds,
|
|
110
110
|
remainingPercent: percent(left / limit * 100), resetAt,
|
|
111
|
-
amounts: {
|
|
111
|
+
amounts: { unit: 'USD', limit, used: spent, remaining: left } }];
|
|
112
112
|
});
|
|
113
113
|
// Footer/detail convention is ascending duration (5h → 1w → 30d).
|
|
114
|
-
return windows.sort((a, b) => a.durationSeconds - b.durationSeconds);
|
|
114
|
+
return windows.sort((a, b) => a.durationSeconds! - b.durationSeconds!);
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
export function parseYesCodeAccount(balancePayload: unknown, profilePayload: unknown): Omit<QuotaSnapshot, 'fetchedAt'> {
|
|
@@ -122,21 +122,20 @@ export function parseYesCodeAccount(balancePayload: unknown, profilePayload: unk
|
|
|
122
122
|
const paygo = optionalAmount(balanceData.pay_as_you_go_balance ?? profile.pay_as_you_go_balance);
|
|
123
123
|
if (subscription === undefined && paygo === undefined) throw new QuotaError('schema');
|
|
124
124
|
|
|
125
|
-
const
|
|
126
|
-
const monthStart = timestamp(profile.last_month_reset);
|
|
127
|
-
const windows = spendingLimitWindows(balanceData) ?? [
|
|
125
|
+
const limits = spendingLimitWindows(balanceData) ?? [
|
|
128
126
|
...limitWindow('daily', '1d', 86400, balanceData.daily_limit, balanceData.daily_spent,
|
|
129
127
|
timestamp(balanceData.daily_resets_at ?? balanceData.daily_reset_at)),
|
|
130
128
|
...limitWindow('weekly', '1w', 604800, balanceData.weekly_limit ?? plan.weekly_limit,
|
|
131
129
|
balanceData.weekly_spent_balance ?? balanceData.weekly_spent ?? profile.current_week_spend,
|
|
132
|
-
timestamp(balanceData.weekly_resets_at ?? balanceData.weekly_reset_at)
|
|
133
|
-
?? (weekStart === null ? null : weekStart + 604800000)),
|
|
130
|
+
timestamp(balanceData.weekly_resets_at ?? balanceData.weekly_reset_at)),
|
|
134
131
|
...limitWindow('monthly', '30d', 2592000, balanceData.monthly_spend_limit ?? plan.monthly_spend_limit,
|
|
135
132
|
balanceData.monthly_spent_balance ?? balanceData.monthly_spent ?? profile.current_month_spend,
|
|
136
|
-
timestamp(balanceData.monthly_resets_at ?? balanceData.monthly_reset_at)
|
|
137
|
-
?? (monthStart === null ? null : monthStart + 2592000000)),
|
|
133
|
+
timestamp(balanceData.monthly_resets_at ?? balanceData.monthly_reset_at)),
|
|
138
134
|
];
|
|
139
|
-
return {
|
|
135
|
+
return { limits, balances: [
|
|
136
|
+
...(subscription !== undefined ? [{ id: 'subscription', label: 'Subscription', currency: 'USD', remaining: subscription }] : []),
|
|
137
|
+
...(paygo !== undefined ? [{ id: 'paygo', label: 'Pay-as-you-go', currency: 'USD', remaining: paygo }] : []),
|
|
138
|
+
], spending: [] };
|
|
140
139
|
}
|
|
141
140
|
|
|
142
141
|
export function createYesCodeAdapter(provider: string, options: YesCodeOptions): QuotaAdapter {
|