@astrosheep/pi-quota 0.5.5 → 0.5.7
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 +1 -1
- package/package.json +1 -1
- package/src/bar/card.ts +119 -9
- package/src/extension.ts +15 -3
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Add bindings in global `~/.pi/agent/settings.json` under `quotaUsage` (not proje
|
|
|
36
36
|
}
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Use the **exact Pi provider ID**. Built-in providers and explicitly bound providers are queried; explicit bindings replace a built-in adapter with the same ID. Unknown keys or adapter options are rejected. The API key and
|
|
39
|
+
Use the **exact Pi provider ID**. Built-in providers and explicitly bound providers are queried; explicit bindings replace a built-in adapter with the same ID. Unknown keys or adapter options are rejected. The API key comes from Pi's provider auth, and the endpoint comes from the provider's registered model; do not put model API keys in `quotaUsage`.
|
|
40
40
|
|
|
41
41
|
| Adapter | Endpoint | Result |
|
|
42
42
|
|---|---|---|
|
package/package.json
CHANGED
package/src/bar/card.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Text } from '@earendil-works/pi-tui';
|
|
1
|
+
import { Text, visibleWidth } from '@earendil-works/pi-tui';
|
|
2
2
|
import type { Component } from '@earendil-works/pi-tui';
|
|
3
|
-
import
|
|
3
|
+
import { duration, formatPercent, horizontalBar, remainingTone } from './bar.ts';
|
|
4
|
+
import type { Paint, Tone } from './bar.ts';
|
|
5
|
+
import { money, remainingMoney } from './balance.ts';
|
|
4
6
|
import { renderUsage } from './quota.ts';
|
|
5
|
-
import type { QuotaState } from '../query/types.ts';
|
|
7
|
+
import type { QuotaErrorCode, QuotaState } from '../query/types.ts';
|
|
6
8
|
|
|
7
9
|
export const USAGE_ENTRY = 'quota-bar:usage:v1';
|
|
8
10
|
|
|
@@ -22,12 +24,120 @@ export function captureAllUsage(states: readonly QuotaState[], now = Date.now())
|
|
|
22
24
|
)) };
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
const errorText: Record<QuotaErrorCode, string> = {
|
|
28
|
+
auth: 'Sign in with /login',
|
|
29
|
+
'unsupported-auth': 'Unsupported credentials or endpoint',
|
|
30
|
+
'account-access': 'Account quota access denied',
|
|
31
|
+
network: 'Network error', timeout: 'Request timed out',
|
|
32
|
+
'rate-limit': 'Rate limited', http: 'Service unavailable', schema: 'Unrecognized quota response',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
interface UsageRow {
|
|
36
|
+
group: number;
|
|
37
|
+
kind: 'quota' | 'balance' | 'error';
|
|
38
|
+
provider: string;
|
|
39
|
+
metric: string;
|
|
40
|
+
value: string;
|
|
41
|
+
secondary: string;
|
|
42
|
+
valueTone: Tone;
|
|
43
|
+
metricTone: Tone;
|
|
44
|
+
secondaryTone: Tone;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function padEndVisible(text: string, width: number): string {
|
|
48
|
+
return text + ' '.repeat(Math.max(0, width - visibleWidth(text)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function padStartVisible(text: string, width: number): string {
|
|
52
|
+
return ' '.repeat(Math.max(0, width - visibleWidth(text))) + text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function gridRows(states: readonly Exclude<QuotaState, { kind: 'loading' }>[], now: number, barWidth: number): UsageRow[] {
|
|
56
|
+
const rows: UsageRow[] = [];
|
|
57
|
+
states.forEach((state, group) => {
|
|
58
|
+
if (state.kind === 'ready') {
|
|
59
|
+
const windows = state.snapshot.windows;
|
|
60
|
+
if (windows.length > 0) {
|
|
61
|
+
windows.forEach((window, index) => {
|
|
62
|
+
const expired = window.resetAt !== null && window.resetAt <= now;
|
|
63
|
+
const remaining = expired ? null : window.remainingPercent;
|
|
64
|
+
rows.push({
|
|
65
|
+
group, kind: 'quota',
|
|
66
|
+
provider: index === 0 ? state.label : '',
|
|
67
|
+
metric: `${window.scope ? `${window.scope}/` : ''}${window.label}`,
|
|
68
|
+
value: `${horizontalBar(remaining, barWidth)} ${formatPercent(remaining)}`,
|
|
69
|
+
secondary: `↺ ${window.resetAt === null ? '?' : duration(window.resetAt - now)}`,
|
|
70
|
+
valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (state.snapshot.balance) {
|
|
76
|
+
const balance = state.snapshot.balance;
|
|
77
|
+
rows.push({
|
|
78
|
+
group, kind: 'balance', provider: state.label, metric: '',
|
|
79
|
+
value: remainingMoney(balance),
|
|
80
|
+
secondary: state.snapshot.spend?.today !== undefined
|
|
81
|
+
? `today ${money(state.snapshot.spend.today, state.snapshot.spend.currency)}` : '',
|
|
82
|
+
valueTone: !balance.unlimited && balance.remaining <= 0 ? 'error' : 'text',
|
|
83
|
+
metricTone: 'dim', secondaryTone: 'dim',
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (state.snapshot.allowance) {
|
|
88
|
+
const allowance = state.snapshot.allowance;
|
|
89
|
+
const remaining = Math.max(0, Math.min(100, allowance.remaining / allowance.limit * 100));
|
|
90
|
+
rows.push({
|
|
91
|
+
group, kind: 'balance', provider: state.label, metric: '',
|
|
92
|
+
value: `${horizontalBar(remaining, barWidth)} ${formatPercent(remaining)}`,
|
|
93
|
+
secondary: `${money(allowance.used, allowance.currency)} used`,
|
|
94
|
+
valueTone: remainingTone(remaining), metricTone: 'dim', secondaryTone: 'dim',
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (state.kind === 'error') {
|
|
100
|
+
rows.push({ group, kind: 'error', provider: state.label, metric: '', value: errorText[state.code], secondary: '',
|
|
101
|
+
valueTone: 'warning', metricTone: 'dim', secondaryTone: 'dim' });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return rows;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function aggregateUsageLines(
|
|
108
|
+
states: readonly Exclude<QuotaState, { kind: 'loading' }>[],
|
|
109
|
+
paint: Paint = (_tone, text) => text,
|
|
110
|
+
now = Date.now(),
|
|
111
|
+
barWidth = 12,
|
|
112
|
+
): string[] {
|
|
113
|
+
const rows = gridRows(states, now, barWidth);
|
|
114
|
+
if (rows.length === 0) return ['No quota data available. Run /usage --all for details.'];
|
|
115
|
+
const quotaRows = rows.filter(row => row.kind === 'quota');
|
|
116
|
+
const otherRows = rows.filter(row => row.kind !== 'quota');
|
|
117
|
+
const providerWidth = Math.max(...quotaRows.map(row => visibleWidth(row.provider)), ...otherRows.map(row => visibleWidth(row.provider)));
|
|
118
|
+
const metricWidth = Math.max(0, ...quotaRows.map(row => visibleWidth(row.metric)));
|
|
119
|
+
const quotaValueWidth = Math.max(0, ...quotaRows.map(row => visibleWidth(row.value)));
|
|
120
|
+
const otherValueWidth = Math.max(0, ...otherRows.map(row => visibleWidth(row.value)));
|
|
121
|
+
const result: string[] = [];
|
|
122
|
+
let previousGroup: number | undefined;
|
|
123
|
+
for (const row of rows) {
|
|
124
|
+
if (previousGroup !== undefined && row.group !== previousGroup && (row.kind === 'quota' || rows.some(other => other.group === previousGroup && other.kind === 'quota'))) result.push('');
|
|
125
|
+
previousGroup = row.group;
|
|
126
|
+
const provider = padEndVisible(row.provider, providerWidth);
|
|
127
|
+
const metric = row.kind === 'quota' ? padEndVisible(row.metric, metricWidth) : '';
|
|
128
|
+
const value = padStartVisible(row.value, row.kind === 'quota' ? quotaValueWidth : otherValueWidth);
|
|
129
|
+
const line = paint('text', provider)
|
|
130
|
+
+ (metric ? paint(row.metricTone, ` ${metric}`) : '')
|
|
131
|
+
+ paint(row.valueTone, ` ${value}`)
|
|
132
|
+
+ (row.secondary ? paint(row.secondaryTone, ` ${row.secondary}`) : '');
|
|
133
|
+
result.push(line.replace(/\s+$/u, ''));
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function usageLines(card: UsageCard, paint?: Paint, barWidth = 12): string[] {
|
|
26
139
|
if (card.version === 1) return renderUsage(card.state, paint, card.capturedAt, barWidth);
|
|
27
|
-
|
|
28
|
-
return card.states.flatMap((state, index) => [
|
|
29
|
-
...(index ? ['', ''] : []), ...renderUsage(state, paint, card.capturedAt, barWidth),
|
|
30
|
-
]);
|
|
140
|
+
return aggregateUsageLines(card.states, paint, card.capturedAt, barWidth);
|
|
31
141
|
}
|
|
32
142
|
|
|
33
143
|
export function usageCardComponent(card: UsageCard | undefined, paint?: Paint): Component {
|
|
@@ -38,7 +148,7 @@ export function usageCardComponent(card: UsageCard | undefined, paint?: Paint):
|
|
|
38
148
|
return new Text('Usage snapshot unavailable.', 0, 0).render(width);
|
|
39
149
|
}
|
|
40
150
|
// Rebuild on every render for theme/width changes, but freeze time at capture.
|
|
41
|
-
const lines = usageLines(card, paint, width < 55 ?
|
|
151
|
+
const lines = usageLines(card, paint, width < 55 ? 6 : 12);
|
|
42
152
|
// Wrapping, not clipping: long quota names remain readable in narrow terminals.
|
|
43
153
|
return new Text(lines.join('\n'), 0, 0).render(width);
|
|
44
154
|
},
|
package/src/extension.ts
CHANGED
|
@@ -85,8 +85,11 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
|
|
|
85
85
|
if (typeof value === 'string') headers[name] = value;
|
|
86
86
|
}
|
|
87
87
|
const auth: ProviderAuth = { ...resolved.auth, headers };
|
|
88
|
-
//
|
|
89
|
-
|
|
88
|
+
// Provider auth often has no endpoint. /usage also queries providers other
|
|
89
|
+
// than the selected model, so resolve their configured model URL as well.
|
|
90
|
+
const model = ctx.model?.provider === provider ? ctx.model
|
|
91
|
+
: ctx.modelRegistry.getAll().find(model => model.provider === provider);
|
|
92
|
+
if (model?.baseUrl) auth.baseUrl = model.baseUrl;
|
|
90
93
|
return auth;
|
|
91
94
|
};
|
|
92
95
|
|
|
@@ -177,11 +180,20 @@ export function createQuotaExtension(options: QuotaExtensionOptions = {}) {
|
|
|
177
180
|
const getAuth = resolveAuth(ctx);
|
|
178
181
|
const providers = supportedProviders(ctx);
|
|
179
182
|
const states = await Promise.all(providers.map(provider => queryProvider(provider, getAuth)));
|
|
183
|
+
const ordered = states
|
|
184
|
+
.map((state, index) => ({ state, index }))
|
|
185
|
+
.sort((a, b) => {
|
|
186
|
+
const rank = (state: QuotaState) => state.kind === 'error' ? 3
|
|
187
|
+
: state.kind === 'ready' && state.snapshot.windows.length > 0 ? 1
|
|
188
|
+
: 2;
|
|
189
|
+
return rank(a.state) - rank(b.state) || a.index - b.index;
|
|
190
|
+
})
|
|
191
|
+
.map(item => item.state);
|
|
180
192
|
// Never append via a stale Pi runtime, or label a replacement model's data
|
|
181
193
|
// as the result of the original command. A newer command owns its result.
|
|
182
194
|
if (!current || generation !== commandGeneration) return;
|
|
183
195
|
const visibleStates = args.trim() === '--all'
|
|
184
|
-
?
|
|
196
|
+
? ordered : ordered.filter(state => state.kind === 'ready');
|
|
185
197
|
const card = captureAllUsage(visibleStates);
|
|
186
198
|
pi.appendEntry<UsageCard>(USAGE_ENTRY, card);
|
|
187
199
|
// Custom entry renderers are TUI-only; RPC clients still get readable output.
|