@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.
@@ -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 { evaluateAgyPreflight, evaluateCodexPreflight } from './quotaPreflight.ts';
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
+ });
@@ -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
- | { readonly ok: false; readonly message: string; readonly resetAt: string | undefined };
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 { ok: false, message: `agy model "${backendModel}" is quota-exhausted`, resetAt };
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 { ok: false, message: 'codex quota limit reached', resetAt: exhaustedWindow.resetAt };
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
- return {
68
- ok: true,
69
- warning: `quota preflight failed (${(err as Error).message}); proceeding`,
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
- return {
80
- ok: true,
81
- warning: `quota preflight failed (${(err as Error).message}); proceeding`,
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
  }