@aibridge/cli 0.3.0 → 0.5.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 +40 -9
- package/dist/cli.mjs +1 -1
- package/dist/{context-B6I4QI9z.mjs → context-F8WLXzPv.mjs} +216 -22
- package/dist/index.d.mts +5 -2
- package/dist/index.mjs +1 -1
- package/package.json +6 -6
- package/src/app.ts +3 -1
- package/src/commands/models/command.ts +18 -0
- package/src/commands/models/impl.test.ts +89 -0
- package/src/commands/models/impl.ts +71 -0
- package/src/commands/quota/command.ts +3 -1
- package/src/commands/quota/impl.ts +24 -2
- package/src/commands/subagent/command.ts +2 -2
- package/src/delegate.test.ts +1 -1
- package/src/driver.ts +6 -1
- package/src/drivers.ts +1 -0
- package/src/flagMapping.test.ts +8 -8
- package/src/models.test.ts +20 -20
- package/src/models.ts +68 -15
- package/src/quotaPreflight.test.ts +74 -1
- package/src/quotaPreflight.ts +59 -13
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import type { AgyQuotaSnapshot } from '@aibridge/driver-agy';
|
|
3
3
|
import type { CodexQuotaSnapshot } from '@aibridge/driver-codex';
|
|
4
|
+
import type { GrokQuotaSnapshot } from '@aibridge/driver-grok';
|
|
4
5
|
import { test } from 'vitest';
|
|
5
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
evaluateAgyPreflight,
|
|
8
|
+
evaluateCodexPreflight,
|
|
9
|
+
evaluateGrokPreflight,
|
|
10
|
+
renderPreflightRefusal,
|
|
11
|
+
} from './quotaPreflight.ts';
|
|
6
12
|
|
|
7
13
|
test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', () => {
|
|
8
14
|
const snapshot: AgyQuotaSnapshot = {
|
|
@@ -23,6 +29,7 @@ test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', ()
|
|
|
23
29
|
|
|
24
30
|
assert.deepEqual(result, {
|
|
25
31
|
ok: false,
|
|
32
|
+
kind: 'quota',
|
|
26
33
|
message: 'agy model "Gemini 3.5 Flash (High)" is quota-exhausted',
|
|
27
34
|
resetAt: '2024-01-02T12:00:00Z',
|
|
28
35
|
});
|
|
@@ -122,6 +129,7 @@ test('evaluateCodexPreflight: limitReached returns ok:false', () => {
|
|
|
122
129
|
|
|
123
130
|
assert.deepEqual(result, {
|
|
124
131
|
ok: false,
|
|
132
|
+
kind: 'quota',
|
|
125
133
|
message: 'codex quota limit reached',
|
|
126
134
|
resetAt: '2024-01-01T17:00:00Z',
|
|
127
135
|
});
|
|
@@ -176,3 +184,68 @@ test('evaluateCodexPreflight: healthy (77% used) returns ok:true', () => {
|
|
|
176
184
|
|
|
177
185
|
assert.deepEqual(result, { ok: true });
|
|
178
186
|
});
|
|
187
|
+
|
|
188
|
+
test('evaluateGrokPreflight: usedPercent at 100 refuses with resetAt', () => {
|
|
189
|
+
const snapshot: GrokQuotaSnapshot = {
|
|
190
|
+
fetchedAt: '2024-01-01T12:00:00Z',
|
|
191
|
+
periodType: 'weekly',
|
|
192
|
+
periodStart: '2024-01-01T00:00:00Z',
|
|
193
|
+
periodEnd: '2024-01-08T00:00:00Z',
|
|
194
|
+
usedPercent: 100,
|
|
195
|
+
products: [],
|
|
196
|
+
onDemandUsedCents: undefined,
|
|
197
|
+
onDemandCapCents: undefined,
|
|
198
|
+
prepaidBalanceCents: undefined,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const result = evaluateGrokPreflight(snapshot);
|
|
202
|
+
|
|
203
|
+
assert.deepEqual(result, {
|
|
204
|
+
ok: false,
|
|
205
|
+
kind: 'quota',
|
|
206
|
+
message: 'grok credit quota exhausted',
|
|
207
|
+
resetAt: '2024-01-08T00:00:00Z',
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('evaluateGrokPreflight: healthy (17% used) returns ok:true', () => {
|
|
212
|
+
const snapshot: GrokQuotaSnapshot = {
|
|
213
|
+
fetchedAt: '2024-01-01T12:00:00Z',
|
|
214
|
+
periodType: 'weekly',
|
|
215
|
+
periodStart: '2024-01-01T00:00:00Z',
|
|
216
|
+
periodEnd: '2024-01-08T00:00:00Z',
|
|
217
|
+
usedPercent: 17,
|
|
218
|
+
products: [],
|
|
219
|
+
onDemandUsedCents: undefined,
|
|
220
|
+
onDemandCapCents: undefined,
|
|
221
|
+
prepaidBalanceCents: undefined,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = evaluateGrokPreflight(snapshot);
|
|
225
|
+
|
|
226
|
+
assert.deepEqual(result, { ok: true });
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('renderPreflightRefusal: auth kind uses unauthenticated wording', () => {
|
|
230
|
+
const msg = renderPreflightRefusal('plan', {
|
|
231
|
+
kind: 'auth',
|
|
232
|
+
message: 'grok session expired (401) — run `grok login`, then retry',
|
|
233
|
+
resetAt: undefined,
|
|
234
|
+
});
|
|
235
|
+
assert.strictEqual(
|
|
236
|
+
msg,
|
|
237
|
+
'aibridge plan: refusing — grok session expired (401) — run `grok login`, then retry. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.',
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('renderPreflightRefusal: quota kind keeps override wording', () => {
|
|
242
|
+
const msg = renderPreflightRefusal('subagent', {
|
|
243
|
+
kind: 'quota',
|
|
244
|
+
message: 'grok credit quota exhausted',
|
|
245
|
+
resetAt: undefined,
|
|
246
|
+
});
|
|
247
|
+
assert.strictEqual(
|
|
248
|
+
msg,
|
|
249
|
+
'aibridge subagent: refusing — grok credit quota exhausted. Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).',
|
|
250
|
+
);
|
|
251
|
+
});
|
package/src/quotaPreflight.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { type AgyQuotaSnapshot, fetchAgyQuota, findModelQuota } from '@aibridge/driver-agy';
|
|
2
2
|
import { type CodexQuotaSnapshot, fetchCodexQuota } from '@aibridge/driver-codex';
|
|
3
|
+
import { fetchGrokQuota, type GrokQuotaSnapshot } from '@aibridge/driver-grok';
|
|
4
|
+
import { isAuthExpired } from '@aibridge/proc';
|
|
3
5
|
import { backendModelId, type ResolvedModel } from './models.ts';
|
|
4
6
|
|
|
5
7
|
export type PreflightVerdict =
|
|
6
8
|
| { readonly ok: true; readonly warning?: string }
|
|
7
|
-
| {
|
|
9
|
+
| {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly kind: 'auth' | 'quota';
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly resetAt: string | undefined;
|
|
14
|
+
};
|
|
8
15
|
|
|
9
16
|
export function evaluateAgyPreflight(
|
|
10
17
|
snapshot: AgyQuotaSnapshot,
|
|
@@ -31,7 +38,12 @@ export function evaluateAgyPreflight(
|
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
}
|
|
34
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
kind: 'quota',
|
|
44
|
+
message: `agy model "${backendModel}" is quota-exhausted`,
|
|
45
|
+
resetAt,
|
|
46
|
+
};
|
|
35
47
|
}
|
|
36
48
|
|
|
37
49
|
return { ok: true };
|
|
@@ -40,12 +52,30 @@ export function evaluateAgyPreflight(
|
|
|
40
52
|
export function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): PreflightVerdict {
|
|
41
53
|
if (snapshot.limitReached) {
|
|
42
54
|
const resetAt = snapshot.windows.find(w => w.resetAt)?.resetAt;
|
|
43
|
-
return { ok: false, message: 'codex quota limit reached', resetAt };
|
|
55
|
+
return { ok: false, kind: 'quota', message: 'codex quota limit reached', resetAt };
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
const exhaustedWindow = snapshot.windows.find(w => w.usedPercent >= 100);
|
|
47
59
|
if (exhaustedWindow) {
|
|
48
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
kind: 'quota',
|
|
63
|
+
message: 'codex quota limit reached',
|
|
64
|
+
resetAt: exhaustedWindow.resetAt,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { ok: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function evaluateGrokPreflight(snapshot: GrokQuotaSnapshot): PreflightVerdict {
|
|
72
|
+
if (snapshot.usedPercent !== undefined && snapshot.usedPercent >= 100) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
kind: 'quota',
|
|
76
|
+
message: 'grok credit quota exhausted',
|
|
77
|
+
resetAt: snapshot.periodEnd,
|
|
78
|
+
};
|
|
49
79
|
}
|
|
50
80
|
|
|
51
81
|
return { ok: true };
|
|
@@ -56,6 +86,10 @@ export async function preflightModel(resolved: ResolvedModel): Promise<Preflight
|
|
|
56
86
|
return preflightCodex();
|
|
57
87
|
}
|
|
58
88
|
|
|
89
|
+
if (resolved.spec.backend === 'grok') {
|
|
90
|
+
return preflightGrok();
|
|
91
|
+
}
|
|
92
|
+
|
|
59
93
|
if (resolved.spec.backend !== 'agy') {
|
|
60
94
|
return { ok: true };
|
|
61
95
|
}
|
|
@@ -64,10 +98,9 @@ export async function preflightModel(resolved: ResolvedModel): Promise<Preflight
|
|
|
64
98
|
const snapshot = await fetchAgyQuota();
|
|
65
99
|
return evaluateAgyPreflight(snapshot, backendModelId(resolved));
|
|
66
100
|
} catch (err) {
|
|
67
|
-
|
|
68
|
-
ok:
|
|
69
|
-
|
|
70
|
-
};
|
|
101
|
+
if (isAuthExpired(err))
|
|
102
|
+
return { ok: false, kind: 'auth', message: err.message, resetAt: undefined };
|
|
103
|
+
return { ok: true, warning: `quota preflight failed (${(err as Error).message}); proceeding` };
|
|
71
104
|
}
|
|
72
105
|
}
|
|
73
106
|
|
|
@@ -76,10 +109,20 @@ export async function preflightCodex(): Promise<PreflightVerdict> {
|
|
|
76
109
|
const snapshot = await fetchCodexQuota();
|
|
77
110
|
return evaluateCodexPreflight(snapshot);
|
|
78
111
|
} catch (err) {
|
|
79
|
-
|
|
80
|
-
ok:
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
if (isAuthExpired(err))
|
|
113
|
+
return { ok: false, kind: 'auth', message: err.message, resetAt: undefined };
|
|
114
|
+
return { ok: true, warning: `quota preflight failed (${(err as Error).message}); proceeding` };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function preflightGrok(): Promise<PreflightVerdict> {
|
|
119
|
+
try {
|
|
120
|
+
const snapshot = await fetchGrokQuota();
|
|
121
|
+
return evaluateGrokPreflight(snapshot);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
if (isAuthExpired(err))
|
|
124
|
+
return { ok: false, kind: 'auth', message: err.message, resetAt: undefined };
|
|
125
|
+
return { ok: true, warning: `quota preflight failed (${(err as Error).message}); proceeding` };
|
|
83
126
|
}
|
|
84
127
|
}
|
|
85
128
|
|
|
@@ -95,8 +138,11 @@ function formatReset(resetTime: string | undefined): string {
|
|
|
95
138
|
|
|
96
139
|
export function renderPreflightRefusal(
|
|
97
140
|
cmd: string,
|
|
98
|
-
verdict: { message: string; resetAt: string | undefined },
|
|
141
|
+
verdict: { kind: 'auth' | 'quota'; message: string; resetAt: string | undefined },
|
|
99
142
|
): string {
|
|
143
|
+
if (verdict.kind === 'auth') {
|
|
144
|
+
return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.`;
|
|
145
|
+
}
|
|
100
146
|
const resetClause = verdict.resetAt ? ` Resets ${formatReset(verdict.resetAt)}.` : '';
|
|
101
147
|
return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).`;
|
|
102
148
|
}
|