@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/dist/query/types.d.ts
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface Balance {
|
|
2
2
|
id: string;
|
|
3
3
|
label: string;
|
|
4
|
-
remainingPercent: number | null;
|
|
5
|
-
resetAt: number | null;
|
|
6
|
-
durationSeconds: number;
|
|
7
|
-
scope?: string;
|
|
8
|
-
amounts?: QuotaAllowance;
|
|
9
|
-
}
|
|
10
|
-
export interface AccountBalance {
|
|
11
4
|
currency: string;
|
|
12
5
|
remaining: number;
|
|
13
|
-
unlimited?: boolean;
|
|
14
6
|
}
|
|
15
|
-
export interface
|
|
16
|
-
|
|
17
|
-
limit: number;
|
|
7
|
+
export interface LimitAmounts {
|
|
8
|
+
unit: string;
|
|
9
|
+
limit: number | null;
|
|
18
10
|
used: number;
|
|
19
11
|
remaining: number;
|
|
20
12
|
}
|
|
21
|
-
export interface
|
|
13
|
+
export interface UsageLimit {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
scope?: string;
|
|
17
|
+
remainingPercent: number | null;
|
|
18
|
+
resetAt: number | null;
|
|
19
|
+
durationSeconds?: number;
|
|
20
|
+
amounts?: LimitAmounts;
|
|
21
|
+
unlimited?: true;
|
|
22
|
+
}
|
|
23
|
+
export interface Spending {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
22
26
|
currency: string;
|
|
23
|
-
|
|
24
|
-
lifetime
|
|
27
|
+
amount: number;
|
|
28
|
+
period: 'today' | 'lifetime';
|
|
25
29
|
}
|
|
26
30
|
export interface QuotaSnapshot {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
spend?: SpendSummary;
|
|
31
|
+
balances: Balance[];
|
|
32
|
+
limits: UsageLimit[];
|
|
33
|
+
spending: Spending[];
|
|
31
34
|
fetchedAt: number;
|
|
32
35
|
}
|
|
33
36
|
export interface ProviderAuth {
|
package/dist/query/yescode.js
CHANGED
|
@@ -62,7 +62,7 @@ function limitWindow(id, label, durationSeconds, limitRaw, usedRaw, resetAt) {
|
|
|
62
62
|
}
|
|
63
63
|
const remaining = Math.max(0, limit - used);
|
|
64
64
|
return [{ id, label, durationSeconds, remainingPercent: percent(remaining / limit * 100), resetAt,
|
|
65
|
-
amounts: {
|
|
65
|
+
amounts: { unit: 'USD', limit, used, remaining } }];
|
|
66
66
|
}
|
|
67
67
|
// GET /api/v1/user/balance (optional; some accounts may lack it) merged with
|
|
68
68
|
// GET /api/v1/auth/profile (required). The live response carries a generic
|
|
@@ -93,7 +93,7 @@ function spendingLimitWindows(balanceData) {
|
|
|
93
93
|
const left = remaining ?? Math.max(0, limit - spent);
|
|
94
94
|
return [{ id: `spending:${item.key}`, label: windowLabel(windowSeconds), durationSeconds: windowSeconds,
|
|
95
95
|
remainingPercent: percent(left / limit * 100), resetAt,
|
|
96
|
-
amounts: {
|
|
96
|
+
amounts: { unit: 'USD', limit, used: spent, remaining: left } }];
|
|
97
97
|
});
|
|
98
98
|
// Footer/detail convention is ascending duration (5h → 1w → 30d).
|
|
99
99
|
return windows.sort((a, b) => a.durationSeconds - b.durationSeconds);
|
|
@@ -106,16 +106,15 @@ export function parseYesCodeAccount(balancePayload, profilePayload) {
|
|
|
106
106
|
const paygo = optionalAmount(balanceData.pay_as_you_go_balance ?? profile.pay_as_you_go_balance);
|
|
107
107
|
if (subscription === undefined && paygo === undefined)
|
|
108
108
|
throw new QuotaError('schema');
|
|
109
|
-
const
|
|
110
|
-
const monthStart = timestamp(profile.last_month_reset);
|
|
111
|
-
const windows = spendingLimitWindows(balanceData) ?? [
|
|
109
|
+
const limits = spendingLimitWindows(balanceData) ?? [
|
|
112
110
|
...limitWindow('daily', '1d', 86400, balanceData.daily_limit, balanceData.daily_spent, timestamp(balanceData.daily_resets_at ?? balanceData.daily_reset_at)),
|
|
113
|
-
...limitWindow('weekly', '1w', 604800, balanceData.weekly_limit ?? plan.weekly_limit, balanceData.weekly_spent_balance ?? balanceData.weekly_spent ?? profile.current_week_spend, timestamp(balanceData.weekly_resets_at ?? balanceData.weekly_reset_at)
|
|
114
|
-
|
|
115
|
-
...limitWindow('monthly', '30d', 2592000, balanceData.monthly_spend_limit ?? plan.monthly_spend_limit, balanceData.monthly_spent_balance ?? balanceData.monthly_spent ?? profile.current_month_spend, timestamp(balanceData.monthly_resets_at ?? balanceData.monthly_reset_at)
|
|
116
|
-
?? (monthStart === null ? null : monthStart + 2592000000)),
|
|
111
|
+
...limitWindow('weekly', '1w', 604800, balanceData.weekly_limit ?? plan.weekly_limit, balanceData.weekly_spent_balance ?? balanceData.weekly_spent ?? profile.current_week_spend, timestamp(balanceData.weekly_resets_at ?? balanceData.weekly_reset_at)),
|
|
112
|
+
...limitWindow('monthly', '30d', 2592000, balanceData.monthly_spend_limit ?? plan.monthly_spend_limit, balanceData.monthly_spent_balance ?? balanceData.monthly_spent ?? profile.current_month_spend, timestamp(balanceData.monthly_resets_at ?? balanceData.monthly_reset_at)),
|
|
117
113
|
];
|
|
118
|
-
return {
|
|
114
|
+
return { limits, balances: [
|
|
115
|
+
...(subscription !== undefined ? [{ id: 'subscription', label: 'Subscription', currency: 'USD', remaining: subscription }] : []),
|
|
116
|
+
...(paygo !== undefined ? [{ id: 'paygo', label: 'Pay-as-you-go', currency: 'USD', remaining: paygo }] : []),
|
|
117
|
+
], spending: [] };
|
|
119
118
|
}
|
|
120
119
|
export function createYesCodeAdapter(provider, options) {
|
|
121
120
|
const credentials = validateYesCodeOptions(options);
|
|
@@ -21,8 +21,10 @@ const customAdapter: QuotaAdapter = {
|
|
|
21
21
|
if (remaining === null) throw new QuotaError('schema');
|
|
22
22
|
return {
|
|
23
23
|
fetchedAt: context.now(),
|
|
24
|
-
|
|
24
|
+
balances: [],
|
|
25
|
+
limits: [{ id: 'daily', label: '1d', durationSeconds: 86400,
|
|
25
26
|
remainingPercent: percent(remaining), resetAt: timestamp(data.reset_at) }],
|
|
27
|
+
spending: [],
|
|
26
28
|
};
|
|
27
29
|
},
|
|
28
30
|
};
|
package/examples/preview.ts
CHANGED
|
@@ -10,10 +10,10 @@ const paint: Paint = process.env.NO_COLOR !== undefined ? (_tone, text) => text
|
|
|
10
10
|
: (tone, text) => `\x1b[${colors[tone]}m${text}\x1b[0m`;
|
|
11
11
|
function mock(short: number, weekly: number): QuotaState {
|
|
12
12
|
return { kind: 'ready', provider: 'openai-codex', label: 'Codex', snapshot: {
|
|
13
|
-
fetchedAt: now,
|
|
13
|
+
fetchedAt: now, balances: [], limits: [
|
|
14
14
|
{ id: '5h', label: '5h', durationSeconds: 18000, remainingPercent: short, resetAt: now + 8100000 },
|
|
15
15
|
{ id: '1w', label: '1w', durationSeconds: 604800, remainingPercent: weekly, resetAt: now + 442800000 },
|
|
16
|
-
],
|
|
16
|
+
], spending: [],
|
|
17
17
|
} };
|
|
18
18
|
}
|
|
19
19
|
console.log('MOCK DATA — no live queries\n');
|
package/package.json
CHANGED
package/src/bar/balance.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
import type { Paint, Span } from './bar.ts';
|
|
1
|
+
import type { Balance, LimitAmounts } from '../query/types.ts';
|
|
2
|
+
import type { Span } from './bar.ts';
|
|
4
3
|
|
|
5
4
|
export function money(amount: number, currency: string): string {
|
|
6
5
|
const sign = amount < 0 ? '-' : '';
|
|
@@ -10,55 +9,18 @@ export function money(amount: number, currency: string): string {
|
|
|
10
9
|
return `${sign}${symbol}${absolute.toFixed(2)}`;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
export function
|
|
14
|
-
|
|
15
|
-
return money(balance.remaining, balance.currency);
|
|
12
|
+
export function quantity(amount: number, unit: string): string {
|
|
13
|
+
return /^[A-Z]{3}$/.test(unit) ? money(amount, unit) : `${amount.toLocaleString('en-US', { maximumFractionDigits: 8 })} ${unit}`;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
export function
|
|
19
|
-
return
|
|
16
|
+
export function amountSummary(amounts: LimitAmounts): string {
|
|
17
|
+
return `${quantity(amounts.remaining, amounts.unit)}/${amounts.limit === null ? '?' : quantity(amounts.limit, amounts.unit)}`
|
|
18
|
+
+ ` · ${quantity(amounts.used, amounts.unit)} used`;
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
export function
|
|
23
|
-
return remainingMoney(balance);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function balanceSpans(balance: AccountBalance): Span[] {
|
|
27
|
-
return [
|
|
28
|
-
{ text: 'Bal ', tone: 'dim' },
|
|
29
|
-
{ text: compactRemainingMoney(balance), tone: !balance.unlimited && balance.remaining <= 0 ? 'error' : 'text' },
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function balanceLines(balance: AccountBalance, paint: Paint): string[] {
|
|
34
|
-
const remaining = remainingMoney(balance);
|
|
35
|
-
return [paint('dim', 'Balance ') + paint(!balance.unlimited && balance.remaining <= 0 ? 'error' : 'text', remaining)];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function allowancePercent(allowance: QuotaAllowance): number {
|
|
39
|
-
return Math.max(0, Math.min(100, allowance.remaining / allowance.limit * 100));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function allowanceSpans(allowance: QuotaAllowance): Span[] {
|
|
43
|
-
const remaining = allowancePercent(allowance);
|
|
44
|
-
return [
|
|
45
|
-
{ text: 'Quota ', tone: 'dim' },
|
|
46
|
-
{ text: `${verticalBar(remaining)} ${compactMoney(allowance.remaining, allowance.currency)}`, tone: remainingTone(remaining) },
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function allowanceLines(allowance: QuotaAllowance, paint: Paint, barWidth: number): string[] {
|
|
51
|
-
const remaining = allowancePercent(allowance);
|
|
21
|
+
export function balanceSpans(balance: Balance, multiple = false): Span[] {
|
|
52
22
|
return [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
paint('dim', `Used ${money(allowance.used, allowance.currency)}`),
|
|
56
|
-
paint('dim', `Limit ${money(allowance.limit, allowance.currency)}`),
|
|
23
|
+
{ text: `${!multiple && balance.label === 'Balance' ? 'Bal' : balance.label} `, tone: 'dim' },
|
|
24
|
+
{ text: money(balance.remaining, balance.currency), tone: balance.remaining <= 0 ? 'error' : 'text' },
|
|
57
25
|
];
|
|
58
26
|
}
|
|
59
|
-
|
|
60
|
-
export function spendLines(spend: SpendSummary, paint: Paint): string[] {
|
|
61
|
-
const lines: string[] = [];
|
|
62
|
-
if (spend.today !== undefined) lines.push(paint('dim', `Today ${money(spend.today, spend.currency)}`));
|
|
63
|
-
return lines;
|
|
64
|
-
}
|
package/src/bar/card.ts
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { Text, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import type { Component } from '@earendil-works/pi-tui';
|
|
3
|
-
import {
|
|
3
|
+
import { formatPercent, horizontalBar, remainingTone } from './bar.ts';
|
|
4
4
|
import type { Paint, Tone } from './bar.ts';
|
|
5
|
-
import {
|
|
6
|
-
import { renderUsage } from './quota.ts';
|
|
5
|
+
import { amountSummary, money } from './balance.ts';
|
|
6
|
+
import { limitLabel, limitRemaining, limitReset, renderUsage } from './quota.ts';
|
|
7
7
|
import type { QuotaErrorCode, QuotaState } from '../query/types.ts';
|
|
8
8
|
|
|
9
|
-
export const USAGE_ENTRY = 'quota-bar:usage:
|
|
9
|
+
export const USAGE_ENTRY = 'quota-bar:usage:v2';
|
|
10
10
|
|
|
11
11
|
export type UsageCard =
|
|
12
12
|
| { version: 1; capturedAt: number; state: Exclude<QuotaState, { kind: 'loading' }> }
|
|
13
|
-
// currentProvider is optional so cards saved before the marker existed still render.
|
|
14
13
|
| { version: 2; capturedAt: number; states: Exclude<QuotaState, { kind: 'loading' }>[]; currentProvider?: string };
|
|
15
14
|
|
|
16
15
|
export function captureUsage(state: QuotaState, now = Date.now()): UsageCard | undefined {
|
|
17
16
|
if (state.kind === 'loading') return undefined;
|
|
18
|
-
// Preserve v1 rendering for cards already saved in sessions.
|
|
19
17
|
return { version: 1, capturedAt: now, state: structuredClone(state) };
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -61,45 +59,36 @@ function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], n
|
|
|
61
59
|
const mark = (entry: { provider: string; label: string }) =>
|
|
62
60
|
currentProvider !== undefined && entry.provider === currentProvider ? `★ ${entry.label}` : entry.label;
|
|
63
61
|
if (state.kind === 'ready') {
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const remaining = expired ? null : window.remainingPercent;
|
|
69
|
-
rows.push({
|
|
70
|
-
group, kind: 'quota',
|
|
71
|
-
provider: index === 0 ? mark(state) : '',
|
|
72
|
-
metric: `${window.scope ? `${window.scope}/` : ''}${window.label}`,
|
|
73
|
-
value: `${horizontalBar(remaining, barWidth)} ${padStartVisible(formatPercent(remaining), 4)}`,
|
|
74
|
-
secondary: `↺ ${window.resetAt === null ? '?' : duration(window.resetAt - now)}`,
|
|
75
|
-
valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
if (state.snapshot.balance) {
|
|
81
|
-
const balance = state.snapshot.balance;
|
|
82
|
-
rows.push({
|
|
83
|
-
group, kind: 'balance', provider: mark(state), metric: '',
|
|
84
|
-
value: remainingMoney(balance),
|
|
85
|
-
secondary: state.snapshot.spend?.today !== undefined
|
|
86
|
-
? `today ${money(state.snapshot.spend.today, state.snapshot.spend.currency)}` : '',
|
|
87
|
-
valueTone: !balance.unlimited && balance.remaining <= 0 ? 'error' : 'text',
|
|
88
|
-
metricTone: 'dim', secondaryTone: 'dim',
|
|
89
|
-
});
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (state.snapshot.allowance) {
|
|
93
|
-
const allowance = state.snapshot.allowance;
|
|
94
|
-
const remaining = Math.max(0, Math.min(100, allowance.remaining / allowance.limit * 100));
|
|
62
|
+
const firstRow = rows.length;
|
|
63
|
+
state.snapshot.limits.forEach(limit => {
|
|
64
|
+
const remaining = limitRemaining(limit, now);
|
|
65
|
+
const reset = limitReset(limit, now);
|
|
95
66
|
rows.push({
|
|
96
|
-
group, kind: '
|
|
97
|
-
value: `${horizontalBar(remaining, barWidth)} ${formatPercent(remaining)}`,
|
|
98
|
-
secondary:
|
|
99
|
-
valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
|
|
67
|
+
group, kind: 'quota', provider: '', metric: limitLabel(limit),
|
|
68
|
+
value: limit.unlimited ? '∞' : `${horizontalBar(remaining, barWidth)} ${padStartVisible(formatPercent(remaining), 4)}`,
|
|
69
|
+
secondary: [reset, limit.amounts ? amountSummary(limit.amounts) : ''].filter(Boolean).join(' · '),
|
|
70
|
+
valueTone: limit.unlimited ? 'text' : remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
|
|
100
71
|
});
|
|
101
|
-
|
|
72
|
+
});
|
|
73
|
+
const balances = state.snapshot.balances;
|
|
74
|
+
balances.forEach(balance => rows.push({
|
|
75
|
+
group, kind: 'balance', provider: '',
|
|
76
|
+
metric: balances.length === 1 && balance.label === 'Balance' ? '' : balance.label,
|
|
77
|
+
value: money(balance.remaining, balance.currency), secondary: '',
|
|
78
|
+
valueTone: balance.remaining <= 0 ? 'error' : 'text', metricTone: 'dim', secondaryTone: 'dim',
|
|
79
|
+
}));
|
|
80
|
+
const today = state.snapshot.spending.filter(entry => entry.period === 'today');
|
|
81
|
+
// Keep the compact single-wallet + Today layout, but never drop other entries.
|
|
82
|
+
if (balances.length === 1 && today.length === 1) {
|
|
83
|
+
rows[rows.length - 1].secondary = `today ${money(today[0].amount, today[0].currency)}`;
|
|
84
|
+
} else {
|
|
85
|
+
today.forEach(entry => rows.push({
|
|
86
|
+
group, kind: 'balance', provider: '', metric: `Today${today.length > 1 ? ` ${entry.label}` : ''}`,
|
|
87
|
+
value: money(entry.amount, entry.currency), secondary: '',
|
|
88
|
+
valueTone: 'dim', metricTone: 'dim', secondaryTone: 'dim',
|
|
89
|
+
}));
|
|
102
90
|
}
|
|
91
|
+
if (rows.length > firstRow) rows[firstRow].provider = mark(state);
|
|
103
92
|
}
|
|
104
93
|
if (state.kind === 'error') {
|
|
105
94
|
rows.push({ group, kind: 'error', provider: mark(state), metric: '', value: errorText[state.code], secondary: '',
|
|
@@ -121,7 +110,7 @@ export function aggregateUsageLines(
|
|
|
121
110
|
const quotaRows = rows.filter(row => row.kind === 'quota');
|
|
122
111
|
const otherRows = rows.filter(row => row.kind !== 'quota');
|
|
123
112
|
const providerWidth = Math.max(...quotaRows.map(row => visibleWidth(row.provider)), ...otherRows.map(row => visibleWidth(row.provider)));
|
|
124
|
-
const metricWidth = Math.max(0, ...
|
|
113
|
+
const metricWidth = Math.max(0, ...rows.map(row => visibleWidth(row.metric)));
|
|
125
114
|
const quotaValueWidth = Math.max(0, ...quotaRows.map(row => visibleWidth(row.value)));
|
|
126
115
|
const otherValueWidth = Math.max(0, ...otherRows.map(row => visibleWidth(row.value)));
|
|
127
116
|
const result: string[] = [];
|
|
@@ -130,7 +119,7 @@ export function aggregateUsageLines(
|
|
|
130
119
|
if (previousGroup !== undefined && row.group !== previousGroup && (row.kind === 'quota' || rows.some(other => other.group === previousGroup && other.kind === 'quota'))) result.push('');
|
|
131
120
|
previousGroup = row.group;
|
|
132
121
|
const provider = padEndVisible(row.provider, providerWidth);
|
|
133
|
-
const metric = row.kind === 'quota' ? padEndVisible(row.metric, metricWidth) : '';
|
|
122
|
+
const metric = row.kind === 'quota' || row.metric ? padEndVisible(row.metric, metricWidth) : '';
|
|
134
123
|
const value = padEndVisible(row.value, row.kind === 'quota' ? quotaValueWidth : otherValueWidth);
|
|
135
124
|
const line = paint('text', provider)
|
|
136
125
|
+ (metric ? paint(row.metricTone, ` ${metric}`) : '')
|
|
@@ -147,7 +136,7 @@ export function usageLines(card: UsageCard, paint?: Paint, barWidth = 12): strin
|
|
|
147
136
|
}
|
|
148
137
|
|
|
149
138
|
// Narrow-client layout (RPC notify bubbles, phones): provider name on its own
|
|
150
|
-
// header line,
|
|
139
|
+
// header line, limits and balances indented below. No wide grid columns, so monospace code
|
|
151
140
|
// blocks never wrap. Paseo's fence blocks have no horizontal scroll: RN Text
|
|
152
141
|
// wraps long lines and a wrapped grid is worse than no grid.
|
|
153
142
|
export function aggregateCompactLines(
|
|
@@ -159,13 +148,13 @@ export function aggregateCompactLines(
|
|
|
159
148
|
): string[] {
|
|
160
149
|
const rows = gridRows(states, now, barWidth, currentProvider);
|
|
161
150
|
if (rows.length === 0) return ['No quota data available. Run /usage --all for details.'];
|
|
162
|
-
const metricWidth = Math.max(0, ...rows.
|
|
151
|
+
const metricWidth = Math.max(0, ...rows.map(row => visibleWidth(row.metric)));
|
|
163
152
|
const lines: string[] = [];
|
|
164
153
|
let previousGroup: number | undefined;
|
|
165
154
|
for (const row of rows) {
|
|
166
155
|
if (previousGroup !== undefined && row.group !== previousGroup) lines.push('');
|
|
167
156
|
previousGroup = row.group;
|
|
168
|
-
if (row.kind === 'quota') {
|
|
157
|
+
if (row.kind === 'quota' || row.metric || !row.provider) {
|
|
169
158
|
if (row.provider) lines.push(paint('text', row.provider));
|
|
170
159
|
const metric = padEndVisible(row.metric, metricWidth);
|
|
171
160
|
const line = ` ${paint(row.metricTone, metric)} ${paint(row.valueTone, row.value)}`
|
package/src/bar/quota.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import { duration, formatPercent, horizontalBar, plain, remainingTone, renderBar, verticalBar } from './bar.ts';
|
|
3
3
|
import type { BarElement, Paint, Span } from './bar.ts';
|
|
4
|
-
import {
|
|
5
|
-
import type { QuotaErrorCode, QuotaState,
|
|
4
|
+
import { amountSummary, balanceSpans, money, quantity } from './balance.ts';
|
|
5
|
+
import type { QuotaErrorCode, QuotaState, UsageLimit } from '../query/types.ts';
|
|
6
6
|
import type { FooterWindows } from '../query/settings.ts';
|
|
7
7
|
|
|
8
8
|
const errorText: Record<QuotaErrorCode, string> = {
|
|
@@ -13,19 +13,34 @@ const errorText: Record<QuotaErrorCode, string> = {
|
|
|
13
13
|
'rate-limit': 'Rate limited', http: 'Service unavailable', schema: 'Unrecognized quota response',
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
|
|
16
|
+
export function limitLabel(limit: UsageLimit): string {
|
|
17
|
+
return `${limit.scope ? `${limit.scope}/` : ''}${limit.label}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function limitRemaining(limit: UsageLimit, now: number): number | null {
|
|
18
21
|
// Reaching the reset deadline is not proof the provider granted a fresh quota.
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
return limit.resetAt !== null && limit.resetAt <= now ? null : limit.remainingPercent;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function limitReset(limit: UsageLimit, now: number): string {
|
|
26
|
+
// No schedule is inferred from duration alone. No duration/reset means unknown.
|
|
27
|
+
return limit.resetAt !== null ? `↺ ${duration(limit.resetAt - now)}`
|
|
28
|
+
: limit.durationSeconds !== undefined ? '↺ ?' : '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function limitSpans(limit: UsageLimit, now: number): Span[] {
|
|
32
|
+
const remaining = limitRemaining(limit, now);
|
|
33
|
+
const value = limit.unlimited ? '∞' : `${verticalBar(remaining)} ${limit.amounts && limit.durationSeconds === undefined
|
|
34
|
+
? quantity(limit.amounts.remaining, limit.amounts.unit) : formatPercent(remaining)}`;
|
|
35
|
+
const reset = limitReset(limit, now);
|
|
21
36
|
return [
|
|
22
|
-
{ text: `${
|
|
23
|
-
{ text:
|
|
24
|
-
{ text: `
|
|
37
|
+
{ text: `${limitLabel(limit)} `, tone: 'dim' },
|
|
38
|
+
{ text: value, tone: limit.unlimited ? 'text' : remainingTone(remaining) },
|
|
39
|
+
...(reset ? [{ text: ` ${reset}`, tone: 'dim' as const }] : []),
|
|
25
40
|
];
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
//
|
|
43
|
+
// Retain the existing setting name; it now caps displayed limits, not wallets.
|
|
29
44
|
export type { FooterWindows };
|
|
30
45
|
|
|
31
46
|
export function quotaElement(state: QuotaState, now = Date.now(), maxWindows: FooterWindows = 2): BarElement {
|
|
@@ -33,17 +48,18 @@ export function quotaElement(state: QuotaState, now = Date.now(), maxWindows: Fo
|
|
|
33
48
|
if (state.kind === 'hidden') return element;
|
|
34
49
|
if (state.kind === 'loading') return { ...element, spans: [{ text: 'Quota …', tone: 'dim' }] };
|
|
35
50
|
if (state.kind === 'error') return { ...element, spans: [{ text: `Quota ! ${errorText[state.code]}`, tone: 'warning' }] };
|
|
36
|
-
const
|
|
37
|
-
const shared =
|
|
38
|
-
const eligible = shared.length ? shared :
|
|
51
|
+
const { balances, limits } = state.snapshot;
|
|
52
|
+
const shared = limits.filter(limit => !limit.scope);
|
|
53
|
+
const eligible = shared.length ? shared : limits;
|
|
39
54
|
const shown = maxWindows === 'all' ? eligible : eligible.slice(0, maxWindows);
|
|
40
|
-
const spans: Span[] =
|
|
41
|
-
|
|
42
|
-
for (const window of shown) {
|
|
55
|
+
const spans: Span[] = [];
|
|
56
|
+
const append = (parts: Span[]) => {
|
|
43
57
|
if (spans.length) spans.push({ text: ' · ', tone: 'dim' });
|
|
44
|
-
spans.push(...
|
|
45
|
-
}
|
|
46
|
-
|
|
58
|
+
spans.push(...parts);
|
|
59
|
+
};
|
|
60
|
+
balances.forEach(balance => append(balanceSpans(balance, balances.length > 1)));
|
|
61
|
+
shown.forEach(limit => append(limitSpans(limit, now)));
|
|
62
|
+
if (limits.length > shown.length) spans.push({ text: ` +${limits.length - shown.length}`, tone: 'dim' });
|
|
47
63
|
return { ...element, spans };
|
|
48
64
|
}
|
|
49
65
|
|
|
@@ -57,38 +73,27 @@ export function renderUsage(state: QuotaState, paint: Paint = plain, now = Date.
|
|
|
57
73
|
const indent = (line: string) => line ? ` ${line}` : line;
|
|
58
74
|
if (state.kind === 'loading') return [title, '', indent('Loading…')];
|
|
59
75
|
if (state.kind === 'error') return [title, '', indent(errorText[state.code])];
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const valueWidth = Math.max(4, ...rows.map(row => visibleWidth(row.value)));
|
|
72
|
-
const lines = [title, '', ...rows.map(row => {
|
|
73
|
-
const tone = remainingTone(row.remaining);
|
|
74
|
-
const value = ' '.repeat(valueWidth - visibleWidth(row.value)) + row.value;
|
|
75
|
-
return indent(paint('dim', `${row.label}${' '.repeat(labelWidth - visibleWidth(row.label))} `)
|
|
76
|
-
+ paint(tone, `${horizontalBar(row.remaining, barWidth)} ${value}`)
|
|
77
|
-
+ paint('dim', ` ↺ ${row.reset}${row.amounts
|
|
78
|
-
? ` · ${money(row.amounts.remaining, row.amounts.currency)}/${money(row.amounts.limit, row.amounts.currency)}`
|
|
79
|
-
: ''}`));
|
|
76
|
+
const { limits, balances, spending } = state.snapshot;
|
|
77
|
+
const labelWidth = Math.max(0, ...limits.map(limit => visibleWidth(limitLabel(limit))));
|
|
78
|
+
const valueWidth = Math.max(4, ...limits.map(limit => visibleWidth(formatPercent(limitRemaining(limit, now)))));
|
|
79
|
+
const lines = [title, '', ...limits.map(limit => {
|
|
80
|
+
const remaining = limitRemaining(limit, now);
|
|
81
|
+
const value = formatPercent(remaining);
|
|
82
|
+
const reset = limitReset(limit, now);
|
|
83
|
+
return indent(paint('dim', `${limitLabel(limit)}${' '.repeat(labelWidth - visibleWidth(limitLabel(limit)))} `)
|
|
84
|
+
+ paint(limit.unlimited ? 'text' : remainingTone(remaining), limit.unlimited ? '∞'
|
|
85
|
+
: `${horizontalBar(remaining, barWidth)} ${' '.repeat(valueWidth - visibleWidth(value))}${value}`)
|
|
86
|
+
+ paint('dim', `${reset ? ` ${reset}` : ''}${limit.amounts ? ` · ${amountSummary(limit.amounts)}` : ''}`));
|
|
80
87
|
})];
|
|
81
|
-
if (
|
|
82
|
-
if (
|
|
83
|
-
lines.push(
|
|
84
|
-
|
|
85
|
-
if (state.snapshot.balance) {
|
|
86
|
-
if (rows.length || state.snapshot.allowance) lines.push('');
|
|
87
|
-
lines.push(...balanceLines(state.snapshot.balance, paint).map(indent));
|
|
88
|
+
if (balances.length) {
|
|
89
|
+
if (limits.length) lines.push('');
|
|
90
|
+
balances.forEach(balance => lines.push(indent(paint('dim', `${balance.label} `)
|
|
91
|
+
+ paint(balance.remaining <= 0 ? 'error' : 'text', money(balance.remaining, balance.currency)))));
|
|
88
92
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
const today = spending.filter(entry => entry.period === 'today');
|
|
94
|
+
if (today.length) {
|
|
95
|
+
if (limits.length || balances.length) lines.push('');
|
|
96
|
+
today.forEach(entry => lines.push(indent(paint('dim', `Today${today.length > 1 ? ` ${entry.label}` : ''} ${money(entry.amount, entry.currency)}`))));
|
|
92
97
|
}
|
|
93
98
|
return lines;
|
|
94
99
|
}
|
package/src/extension.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { createJsonClient, createPostJsonClient } from './query/http.ts';
|
|
|
8
8
|
import { composeQuotaRegistry } from './query/settings.ts';
|
|
9
9
|
import type { ProviderAuth, QuotaAdapter, QuotaState } from './query/types.ts';
|
|
10
10
|
|
|
11
|
-
export const STATUS_EVENT = 'quota-bar:state:
|
|
11
|
+
export const STATUS_EVENT = 'quota-bar:state:v2';
|
|
12
12
|
const STATUS_KEY = 'quota-bar';
|
|
13
13
|
const USAGE_WIDGET = 'quota-bar:usage';
|
|
14
14
|
const REFRESHING_TEXT = 'Refreshing…';
|
|
@@ -21,7 +21,7 @@ export interface QuotaExtensionOptions {
|
|
|
21
21
|
providers?: readonly string[]; // Extra configured quota providers to include in /usage
|
|
22
22
|
aliases?: Record<string, string>; // alias provider → target provider; shares the target's account quota
|
|
23
23
|
footer?: boolean; // false: query + /usage + structured events only
|
|
24
|
-
footerWindows?: FooterWindows; // footer
|
|
24
|
+
footerWindows?: FooterWindows; // footer limit cap; default 2, 'all' shows every limit
|
|
25
25
|
intervalMs?: number;
|
|
26
26
|
timeoutMs?: number;
|
|
27
27
|
}
|
|
@@ -222,7 +222,7 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
|
|
|
222
222
|
return provider === currentProvider || provider === canonicalProvider ? 0 : 1;
|
|
223
223
|
};
|
|
224
224
|
const rank = (state: QuotaState) => state.kind === 'error' ? 3
|
|
225
|
-
: state.kind === 'ready' && state.snapshot.
|
|
225
|
+
: state.kind === 'ready' && state.snapshot.limits.length > 0 ? 1
|
|
226
226
|
: 2;
|
|
227
227
|
return pinned(a) - pinned(b) || rank(a.state) - rank(b.state) || a.index - b.index;
|
|
228
228
|
})
|
package/src/query/codex.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bearer, header, officialAuth } from './http.ts';
|
|
2
2
|
import { numeric, object, percent, safeLabel, 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
|
// Decode only to obtain routing metadata from the same resolved token.
|
|
7
7
|
// This does not verify a JWT; the official server verifies authentication.
|
|
@@ -13,9 +13,9 @@ export function codexAccountId(token: string): string | undefined {
|
|
|
13
13
|
} catch { return undefined; }
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function parseCodex(payload: unknown, now: number):
|
|
16
|
+
export function parseCodex(payload: unknown, now: number): UsageLimit[] {
|
|
17
17
|
const data = object(payload);
|
|
18
|
-
const windows:
|
|
18
|
+
const windows: UsageLimit[] = [];
|
|
19
19
|
const addDomain = (raw: unknown, domain: string, scope?: string) => {
|
|
20
20
|
const rate = object(raw);
|
|
21
21
|
for (const [key, fallback] of [['primary_window', 18000], ['secondary_window', 604800]] as const) {
|
|
@@ -60,6 +60,6 @@ export const codexAdapter: QuotaAdapter = {
|
|
|
60
60
|
Origin: 'https://chatgpt.com', Referer: 'https://chatgpt.com/',
|
|
61
61
|
}, context.signal);
|
|
62
62
|
const now = context.now();
|
|
63
|
-
return {
|
|
63
|
+
return { balances: [], limits: parseCodex(data, now), spending: [], fetchedAt: now };
|
|
64
64
|
},
|
|
65
65
|
};
|
package/src/query/deepseek.ts
CHANGED
|
@@ -1,25 +1,24 @@
|
|
|
1
1
|
import { bearer, officialAuth } from './http.ts';
|
|
2
2
|
import { numeric, object } from './parse.ts';
|
|
3
3
|
import { QuotaError } from './types.ts';
|
|
4
|
-
import type {
|
|
4
|
+
import type { Balance, QuotaAdapter } from './types.ts';
|
|
5
5
|
|
|
6
6
|
export const DEEPSEEK_ORIGIN = 'https://api.deepseek.com';
|
|
7
7
|
|
|
8
8
|
// https://api-docs.deepseek.com/api/get-user-balance
|
|
9
9
|
// GET /user/balance (also mounted under /v1/). The sk- key works directly.
|
|
10
10
|
// Amounts arrive as strings in yuan; balance_infos may list several currencies.
|
|
11
|
-
export function parseDeepSeekBalance(payload: unknown):
|
|
11
|
+
export function parseDeepSeekBalance(payload: unknown): Balance[] {
|
|
12
12
|
const body = object(payload);
|
|
13
13
|
if (!Array.isArray(body.balance_infos) || body.balance_infos.length === 0) throw new QuotaError('schema');
|
|
14
|
-
|
|
14
|
+
return body.balance_infos.map(entry => {
|
|
15
15
|
const info = object(entry);
|
|
16
16
|
const remaining = numeric(info.total_balance);
|
|
17
17
|
if (typeof info.currency !== 'string' || !/^[A-Z]{3}$/.test(info.currency)
|
|
18
|
-
|| remaining === null
|
|
19
|
-
//
|
|
20
|
-
return { currency: info.currency, remaining };
|
|
21
|
-
}
|
|
22
|
-
throw new QuotaError('schema');
|
|
18
|
+
|| remaining === null) throw new QuotaError('schema');
|
|
19
|
+
// Preserve each currency; the endpoint exposes no usage total.
|
|
20
|
+
return { id: info.currency, label: 'Balance', currency: info.currency, remaining };
|
|
21
|
+
});
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
export function createDeepSeekAdapter(provider: string): QuotaAdapter {
|
|
@@ -29,7 +28,7 @@ export function createDeepSeekAdapter(provider: string): QuotaAdapter {
|
|
|
29
28
|
const auth = await officialAuth(context, DEEPSEEK_ORIGIN);
|
|
30
29
|
const payload = await context.getJson(`${DEEPSEEK_ORIGIN}/user/balance`,
|
|
31
30
|
{ Authorization: `Bearer ${bearer(auth)}` }, context.signal);
|
|
32
|
-
return {
|
|
31
|
+
return { balances: parseDeepSeekBalance(payload), limits: [], spending: [], fetchedAt: context.now() };
|
|
33
32
|
},
|
|
34
33
|
};
|
|
35
34
|
}
|
package/src/query/kimi.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
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
|
const units: Record<string, number> = {
|
|
7
7
|
TIME_UNIT_SECOND: 1, TIME_UNIT_MINUTE: 60, TIME_UNIT_HOUR: 3600, TIME_UNIT_DAY: 86400,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
export function parseKimi(payload: unknown):
|
|
10
|
+
export function parseKimi(payload: unknown): UsageLimit[] {
|
|
11
11
|
const data = object(payload);
|
|
12
|
-
const windows:
|
|
12
|
+
const windows: UsageLimit[] = [];
|
|
13
13
|
const add = (raw: unknown, duration: number, id: string) => {
|
|
14
14
|
const detail = object(raw);
|
|
15
15
|
const limit = numeric(detail.limit);
|
|
@@ -38,7 +38,7 @@ export function parseKimi(payload: unknown): QuotaWindow[] {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
if (windows.length === 0) throw new QuotaError('schema');
|
|
41
|
-
return windows.sort((a, b) => a.durationSeconds - b.durationSeconds);
|
|
41
|
+
return windows.sort((a, b) => a.durationSeconds! - b.durationSeconds!);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export const kimiAdapter: QuotaAdapter = {
|
|
@@ -48,6 +48,6 @@ export const kimiAdapter: QuotaAdapter = {
|
|
|
48
48
|
const data = await context.getJson('https://api.kimi.com/coding/v1/usages', {
|
|
49
49
|
Authorization: `Bearer ${bearer(auth)}`,
|
|
50
50
|
}, context.signal);
|
|
51
|
-
return {
|
|
51
|
+
return { balances: [], limits: parseKimi(data), spending: [], fetchedAt: context.now() };
|
|
52
52
|
},
|
|
53
53
|
};
|