@aibridge/cli 0.4.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 +9 -9
- package/dist/cli.mjs +1 -1
- package/dist/{context-v_ns4n_W.mjs → context-F8WLXzPv.mjs} +48 -2
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/package.json +6 -6
- package/src/commands/subagent/command.ts +1 -1
- package/src/delegate.test.ts +1 -1
- package/src/flagMapping.test.ts +8 -8
- package/src/models.test.ts +20 -20
- package/src/models.ts +20 -0
- package/src/quotaPreflight.test.ts +28 -0
- package/src/quotaPreflight.ts +34 -17
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ npx skills add ycmjason/aibridge
|
|
|
28
28
|
That's it. The skill runs the CLI on demand via `npx -y @aibridge/cli` — nothing else to install. Ask your agent to "use aibridge", or try it yourself:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npx -y @aibridge/cli subagent --model xai-grok/grok-4.
|
|
31
|
+
npx -y @aibridge/cli subagent --model xai-grok/grok-4.6 "summarize the architecture of this repo"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
<sup>Want the `aibridge` command on your PATH for manual use? `npm i -g @aibridge/cli` (optional).</sup>
|
|
@@ -37,10 +37,10 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.5 "summarize the architect
|
|
|
37
37
|
|
|
38
38
|
| Command | Use when |
|
|
39
39
|
|---|---|
|
|
40
|
-
| `aibridge plan --model xai-grok/grok-4.
|
|
41
|
-
| `aibridge implement --model google-antigravity/gemini-3.
|
|
42
|
-
| `aibridge review --model xai-grok/grok-4.
|
|
43
|
-
| `aibridge subagent --model xai-grok/grok-4.
|
|
40
|
+
| `aibridge plan --model xai-grok/grok-4.6 --out plan.md "<task>"` | You want a delegate model to study the repo and expand a task into a detailed, reviewable **plan file** before any code is written |
|
|
41
|
+
| `aibridge implement --model google-antigravity/gemini-3.7-flash <plan.md>` | You have an approved plan file and want it executed in place — with your project's **real typecheck and tests** run until green |
|
|
42
|
+
| `aibridge review --model xai-grok/grok-4.6 --out review.md [--plan <plan.md>]` | You want a **different model** to pressure-test the working-tree diff against the plan contract (over-reach is a finding) — or to review the plan itself before implementing |
|
|
43
|
+
| `aibridge subagent --model xai-grok/grok-4.6 "<task>"` | A self-contained task deserves a concurrent delegate, a cross-model second opinion, or a red-team pass |
|
|
44
44
|
| `aibridge image-gen --model openai-codex/gpt-5.6-sol --out out.png "<prompt>"` | You need a real raster image — on a Codex, Antigravity, or Grok seat, with render verification |
|
|
45
45
|
| `aibridge models [--json]` | You need the exact facts for every registered model seat (accepted efforts, image format, pinned model ID) |
|
|
46
46
|
| `aibridge quota` | Two-second check of every backend's remaining quota before you pipeline work |
|
|
@@ -49,10 +49,10 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.5 "summarize the architect
|
|
|
49
49
|
The three verbs compose into an orchestrator-driven loop your agent stays in charge of:
|
|
50
50
|
|
|
51
51
|
```
|
|
52
|
-
aibridge plan --model xai-grok/grok-4.
|
|
52
|
+
aibridge plan --model xai-grok/grok-4.6 --out plan.md "add rate limiting to the API" # delegate writes plan.md
|
|
53
53
|
# → your agent reads, edits, approves the plan
|
|
54
|
-
aibridge implement --model google-antigravity/gemini-3.
|
|
55
|
-
aibridge review --model xai-grok/grok-4.
|
|
54
|
+
aibridge implement --model google-antigravity/gemini-3.7-flash plan.md # another model executes it, runs your gates
|
|
55
|
+
aibridge review --model xai-grok/grok-4.6 --out review.md --plan plan.md # a third seat cross-checks the diff
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Plan files — not their contents — travel between stages, so the loop is nearly free on your agent's context.
|
|
@@ -62,7 +62,7 @@ Plan files — not their contents — travel between stages, so the loop is near
|
|
|
62
62
|
- **The skill carries judgment; the CLI owns execution.** The skill teaches your agent prompt-craft, seat selection, and when to gate; the CLI deterministically drives the backing CLIs, captures their output, verifies results (a "generated image" under 100 KB is a code-drawn fake, an empty answer is a quota death), and logs every run.
|
|
63
63
|
- **Seats stay cross-model by default.** Grok plans and reviews, Gemini implements — a model never reviews its own diff, and independent eyes catch what shared blind spots miss.
|
|
64
64
|
- **No API keys.** Delegation runs on the backing CLIs' existing logins, each spending its own quota. (The skill treats a backend that shares your agent's own quota pool as a last resort.)
|
|
65
|
-
- **Models are canonical slugs**: `<vendor>-<cli>/<model>[-<effort>]` — e.g. `xai-grok/grok-4.
|
|
65
|
+
- **Models are canonical slugs**: `<vendor>-<cli>/<model>[-<effort>]` — e.g. `xai-grok/grok-4.6`, `google-antigravity/gemini-3.7-flash`, `openai-codex/gpt-5.6-sol-high`, `anthropic-claude/opus-5`. No aliases — not short ones, and not moving vendor aliases like `opus`: every seat pins an exact model version. `aibridge <command> --help` lists every seat.
|
|
66
66
|
|
|
67
67
|
## Tell your agent when to reach for it
|
|
68
68
|
|
package/dist/cli.mjs
CHANGED
|
@@ -11,10 +11,21 @@ import * as codex from "@aibridge/driver-codex";
|
|
|
11
11
|
import { fetchCodexQuota } from "@aibridge/driver-codex";
|
|
12
12
|
import * as grok from "@aibridge/driver-grok";
|
|
13
13
|
import { fetchGrokQuota } from "@aibridge/driver-grok";
|
|
14
|
-
import { runCaptured } from "@aibridge/proc";
|
|
14
|
+
import { isAuthExpired, runCaptured } from "@aibridge/proc";
|
|
15
15
|
import { randomBytes } from "node:crypto";
|
|
16
16
|
//#region src/models.ts
|
|
17
17
|
const MODELS = {
|
|
18
|
+
"xai-grok/grok-4.6": {
|
|
19
|
+
slug: "xai-grok/grok-4.6",
|
|
20
|
+
backend: "grok",
|
|
21
|
+
backendModel: "grok-4.6",
|
|
22
|
+
efforts: [
|
|
23
|
+
"low",
|
|
24
|
+
"medium",
|
|
25
|
+
"high"
|
|
26
|
+
],
|
|
27
|
+
brief: "xAI Grok 4.6 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight"
|
|
28
|
+
},
|
|
18
29
|
"xai-grok/grok-4.5": {
|
|
19
30
|
slug: "xai-grok/grok-4.5",
|
|
20
31
|
backend: "grok",
|
|
@@ -26,6 +37,18 @@ const MODELS = {
|
|
|
26
37
|
],
|
|
27
38
|
brief: "xAI Grok 4.5 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight"
|
|
28
39
|
},
|
|
40
|
+
"google-antigravity/gemini-3.7-flash": {
|
|
41
|
+
slug: "google-antigravity/gemini-3.7-flash",
|
|
42
|
+
backend: "agy",
|
|
43
|
+
backendModel: "gemini-3.7-flash",
|
|
44
|
+
efforts: [
|
|
45
|
+
"low",
|
|
46
|
+
"medium",
|
|
47
|
+
"high"
|
|
48
|
+
],
|
|
49
|
+
defaultEffort: "high",
|
|
50
|
+
brief: "Google Gemini 3.7 Flash via agy — own Antigravity login; quota shared across all Gemini tiers (not yet itemised per-model, so no exhaustion preflight)"
|
|
51
|
+
},
|
|
29
52
|
"google-antigravity/gemini-3.6-flash": {
|
|
30
53
|
slug: "google-antigravity/gemini-3.6-flash",
|
|
31
54
|
backend: "agy",
|
|
@@ -515,6 +538,7 @@ function evaluateAgyPreflight(snapshot, backendModel) {
|
|
|
515
538
|
}
|
|
516
539
|
return {
|
|
517
540
|
ok: false,
|
|
541
|
+
kind: "quota",
|
|
518
542
|
message: `agy model "${backendModel}" is quota-exhausted`,
|
|
519
543
|
resetAt
|
|
520
544
|
};
|
|
@@ -524,12 +548,14 @@ function evaluateAgyPreflight(snapshot, backendModel) {
|
|
|
524
548
|
function evaluateCodexPreflight(snapshot) {
|
|
525
549
|
if (snapshot.limitReached) return {
|
|
526
550
|
ok: false,
|
|
551
|
+
kind: "quota",
|
|
527
552
|
message: "codex quota limit reached",
|
|
528
553
|
resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
|
|
529
554
|
};
|
|
530
555
|
const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
|
|
531
556
|
if (exhaustedWindow) return {
|
|
532
557
|
ok: false,
|
|
558
|
+
kind: "quota",
|
|
533
559
|
message: "codex quota limit reached",
|
|
534
560
|
resetAt: exhaustedWindow.resetAt
|
|
535
561
|
};
|
|
@@ -538,6 +564,7 @@ function evaluateCodexPreflight(snapshot) {
|
|
|
538
564
|
function evaluateGrokPreflight(snapshot) {
|
|
539
565
|
if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
|
|
540
566
|
ok: false,
|
|
567
|
+
kind: "quota",
|
|
541
568
|
message: "grok credit quota exhausted",
|
|
542
569
|
resetAt: snapshot.periodEnd
|
|
543
570
|
};
|
|
@@ -550,6 +577,12 @@ async function preflightModel(resolved) {
|
|
|
550
577
|
try {
|
|
551
578
|
return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
|
|
552
579
|
} catch (err) {
|
|
580
|
+
if (isAuthExpired(err)) return {
|
|
581
|
+
ok: false,
|
|
582
|
+
kind: "auth",
|
|
583
|
+
message: err.message,
|
|
584
|
+
resetAt: void 0
|
|
585
|
+
};
|
|
553
586
|
return {
|
|
554
587
|
ok: true,
|
|
555
588
|
warning: `quota preflight failed (${err.message}); proceeding`
|
|
@@ -560,6 +593,12 @@ async function preflightCodex() {
|
|
|
560
593
|
try {
|
|
561
594
|
return evaluateCodexPreflight(await fetchCodexQuota());
|
|
562
595
|
} catch (err) {
|
|
596
|
+
if (isAuthExpired(err)) return {
|
|
597
|
+
ok: false,
|
|
598
|
+
kind: "auth",
|
|
599
|
+
message: err.message,
|
|
600
|
+
resetAt: void 0
|
|
601
|
+
};
|
|
563
602
|
return {
|
|
564
603
|
ok: true,
|
|
565
604
|
warning: `quota preflight failed (${err.message}); proceeding`
|
|
@@ -570,6 +609,12 @@ async function preflightGrok() {
|
|
|
570
609
|
try {
|
|
571
610
|
return evaluateGrokPreflight(await fetchGrokQuota());
|
|
572
611
|
} catch (err) {
|
|
612
|
+
if (isAuthExpired(err)) return {
|
|
613
|
+
ok: false,
|
|
614
|
+
kind: "auth",
|
|
615
|
+
message: err.message,
|
|
616
|
+
resetAt: void 0
|
|
617
|
+
};
|
|
573
618
|
return {
|
|
574
619
|
ok: true,
|
|
575
620
|
warning: `quota preflight failed (${err.message}); proceeding`
|
|
@@ -586,6 +631,7 @@ function formatReset$1(resetTime) {
|
|
|
586
631
|
return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
|
|
587
632
|
}
|
|
588
633
|
function renderPreflightRefusal(cmd, verdict) {
|
|
634
|
+
if (verdict.kind === "auth") return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.`;
|
|
589
635
|
const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
|
|
590
636
|
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).`;
|
|
591
637
|
}
|
|
@@ -1537,7 +1583,7 @@ const fullDescription = [
|
|
|
1537
1583
|
"",
|
|
1538
1584
|
"Available models (canonical slug):",
|
|
1539
1585
|
...listModelHelpLines(),
|
|
1540
|
-
"Recommended first choice: xai-grok/grok-4.
|
|
1586
|
+
"Recommended first choice: xai-grok/grok-4.6. Whichever seat runs on the same provider as the",
|
|
1541
1587
|
"agent you orchestrate from is your last resort — it spends the pool you are already burning."
|
|
1542
1588
|
].join("\n");
|
|
1543
1589
|
const subagent = buildCommand({
|
package/dist/index.d.mts
CHANGED
|
@@ -165,6 +165,7 @@ type PreflightVerdict = {
|
|
|
165
165
|
readonly warning?: string;
|
|
166
166
|
} | {
|
|
167
167
|
readonly ok: false;
|
|
168
|
+
readonly kind: 'auth' | 'quota';
|
|
168
169
|
readonly message: string;
|
|
169
170
|
readonly resetAt: string | undefined;
|
|
170
171
|
};
|
|
@@ -173,6 +174,7 @@ declare function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): Preflight
|
|
|
173
174
|
declare function preflightModel(resolved: ResolvedModel): Promise<PreflightVerdict>;
|
|
174
175
|
declare function preflightCodex(): Promise<PreflightVerdict>;
|
|
175
176
|
declare function renderPreflightRefusal(cmd: string, verdict: {
|
|
177
|
+
kind: 'auth' | 'quota';
|
|
176
178
|
message: string;
|
|
177
179
|
resetAt: string | undefined;
|
|
178
180
|
}): string;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateCodexPreflight, d as renderPreflightRefusal, f as delegate, g as MODELS, h as positiveIntSeconds, i as listRuns, l as preflightCodex, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as evaluateAgyPreflight, t as buildContext, u as preflightModel, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-
|
|
1
|
+
import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateCodexPreflight, d as renderPreflightRefusal, f as delegate, g as MODELS, h as positiveIntSeconds, i as listRuns, l as preflightCodex, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as evaluateAgyPreflight, t as buildContext, u as preflightModel, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-F8WLXzPv.mjs";
|
|
2
2
|
export { MODELS, app, backendModelId, buildContext, delegate, evaluateAgyPreflight, evaluateCodexPreflight, formatImageGenModelError, formatUnknownModelError, getDriver, listModelHelpLines, listRuns, nonEmptyPrompt, positiveIntSeconds, preflightCodex, preflightModel, readRunLogs, renderPreflightRefusal, resolveModel, runCli, startRun, supportsImageGen };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibridge/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI that bridges tasks to AI CLIs on your machine (plan / implement / review / subagent / image-gen)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,11 +35,11 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@stricli/core": "1.3.0",
|
|
38
|
-
"@aibridge/proc": "0.
|
|
39
|
-
"@aibridge/driver-
|
|
40
|
-
"@aibridge/driver-
|
|
41
|
-
"@aibridge/driver-
|
|
42
|
-
"@aibridge/driver-
|
|
38
|
+
"@aibridge/proc": "0.5.0",
|
|
39
|
+
"@aibridge/driver-codex": "0.5.0",
|
|
40
|
+
"@aibridge/driver-claude": "0.5.0",
|
|
41
|
+
"@aibridge/driver-agy": "0.5.0",
|
|
42
|
+
"@aibridge/driver-grok": "0.5.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"tsdown": "0.22.14"
|
|
@@ -8,7 +8,7 @@ const fullDescription = [
|
|
|
8
8
|
'',
|
|
9
9
|
'Available models (canonical slug):',
|
|
10
10
|
...listModelHelpLines(),
|
|
11
|
-
'Recommended first choice: xai-grok/grok-4.
|
|
11
|
+
'Recommended first choice: xai-grok/grok-4.6. Whichever seat runs on the same provider as the',
|
|
12
12
|
'agent you orchestrate from is your last resort — it spends the pool you are already burning.',
|
|
13
13
|
].join('\n');
|
|
14
14
|
|
package/src/delegate.test.ts
CHANGED
|
@@ -64,7 +64,7 @@ function createRecordingRunLog() {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
describe('delegate stub-driver tests', () => {
|
|
67
|
-
const model = resolveModel('xai-grok/grok-4.
|
|
67
|
+
const model = resolveModel('xai-grok/grok-4.6');
|
|
68
68
|
if (!model) throw new Error('model resolution failed');
|
|
69
69
|
|
|
70
70
|
it('prepends preamble when tools: true, passes untouched when tools: false', async () => {
|
package/src/flagMapping.test.ts
CHANGED
|
@@ -35,7 +35,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
35
35
|
it('plan command maps defaults correctly', async () => {
|
|
36
36
|
mockPlanImpl.mockReset();
|
|
37
37
|
const ctx = fakeCtx();
|
|
38
|
-
await runCli(ctx, ['plan', '--model', 'xai-grok/grok-4.
|
|
38
|
+
await runCli(ctx, ['plan', '--model', 'xai-grok/grok-4.6', '--out', 'plan.md', 'do something']);
|
|
39
39
|
expect(mockPlanImpl).toHaveBeenCalledTimes(1);
|
|
40
40
|
const [call] = mockPlanImpl.mock.calls;
|
|
41
41
|
expect(call).toBeDefined();
|
|
@@ -43,7 +43,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
43
43
|
const [flags, prompt] = call;
|
|
44
44
|
expect(prompt).toBe('do something');
|
|
45
45
|
expect(flags).toEqual({
|
|
46
|
-
model: 'xai-grok/grok-4.
|
|
46
|
+
model: 'xai-grok/grok-4.6',
|
|
47
47
|
out: 'plan.md',
|
|
48
48
|
preflight: true,
|
|
49
49
|
});
|
|
@@ -55,7 +55,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
55
55
|
await runCli(ctx, [
|
|
56
56
|
'plan',
|
|
57
57
|
'--model',
|
|
58
|
-
'xai-grok/grok-4.
|
|
58
|
+
'xai-grok/grok-4.6',
|
|
59
59
|
'--out',
|
|
60
60
|
'plan.md',
|
|
61
61
|
'--no-preflight',
|
|
@@ -70,7 +70,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
70
70
|
const [flags, prompt] = call;
|
|
71
71
|
expect(prompt).toBe('task');
|
|
72
72
|
expect(flags).toEqual({
|
|
73
|
-
model: 'xai-grok/grok-4.
|
|
73
|
+
model: 'xai-grok/grok-4.6',
|
|
74
74
|
out: 'plan.md',
|
|
75
75
|
preflight: false,
|
|
76
76
|
timeout: 120,
|
|
@@ -80,7 +80,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
80
80
|
it('subagent command maps defaults correctly', async () => {
|
|
81
81
|
mockSubagentImpl.mockReset();
|
|
82
82
|
const ctx = fakeCtx();
|
|
83
|
-
await runCli(ctx, ['subagent', '--model', 'xai-grok/grok-4.
|
|
83
|
+
await runCli(ctx, ['subagent', '--model', 'xai-grok/grok-4.6', 'hello agent']);
|
|
84
84
|
expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
|
|
85
85
|
const [call] = mockSubagentImpl.mock.calls;
|
|
86
86
|
expect(call).toBeDefined();
|
|
@@ -88,7 +88,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
88
88
|
const [flags, prompt] = call;
|
|
89
89
|
expect(prompt).toBe('hello agent');
|
|
90
90
|
expect(flags).toEqual({
|
|
91
|
-
model: 'xai-grok/grok-4.
|
|
91
|
+
model: 'xai-grok/grok-4.6',
|
|
92
92
|
tools: true,
|
|
93
93
|
preflight: true,
|
|
94
94
|
json: false,
|
|
@@ -101,7 +101,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
101
101
|
await runCli(ctx, [
|
|
102
102
|
'subagent',
|
|
103
103
|
'--model',
|
|
104
|
-
'xai-grok/grok-4.
|
|
104
|
+
'xai-grok/grok-4.6',
|
|
105
105
|
'--no-tools',
|
|
106
106
|
'--no-preflight',
|
|
107
107
|
'hello agent',
|
|
@@ -113,7 +113,7 @@ describe('flag mapping & defaults lock', () => {
|
|
|
113
113
|
const [flags, prompt] = call;
|
|
114
114
|
expect(prompt).toBe('hello agent');
|
|
115
115
|
expect(flags).toEqual({
|
|
116
|
-
model: 'xai-grok/grok-4.
|
|
116
|
+
model: 'xai-grok/grok-4.6',
|
|
117
117
|
tools: false,
|
|
118
118
|
preflight: false,
|
|
119
119
|
json: false,
|
package/src/models.test.ts
CHANGED
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
|
|
11
11
|
describe('models registry', () => {
|
|
12
12
|
it('resolves canonical slugs', () => {
|
|
13
|
-
const grok = resolveModel('xai-grok/grok-4.
|
|
13
|
+
const grok = resolveModel('xai-grok/grok-4.6');
|
|
14
14
|
expect(grok).toBeDefined();
|
|
15
|
-
expect(grok?.spec.slug).toBe('xai-grok/grok-4.
|
|
15
|
+
expect(grok?.spec.slug).toBe('xai-grok/grok-4.6');
|
|
16
16
|
expect(grok?.effort).toBeUndefined();
|
|
17
17
|
|
|
18
|
-
const gemini = resolveModel('google-antigravity/gemini-3.
|
|
18
|
+
const gemini = resolveModel('google-antigravity/gemini-3.7-flash');
|
|
19
19
|
expect(gemini).toBeDefined();
|
|
20
|
-
expect(gemini?.spec.slug).toBe('google-antigravity/gemini-3.
|
|
20
|
+
expect(gemini?.spec.slug).toBe('google-antigravity/gemini-3.7-flash');
|
|
21
21
|
expect(gemini?.effort).toBe('high');
|
|
22
22
|
});
|
|
23
23
|
|
|
@@ -38,8 +38,8 @@ describe('models registry', () => {
|
|
|
38
38
|
});
|
|
39
39
|
|
|
40
40
|
it('resolves effort suffixes', () => {
|
|
41
|
-
const grokMedium = resolveModel('xai-grok/grok-4.
|
|
42
|
-
expect(grokMedium?.spec.slug).toBe('xai-grok/grok-4.
|
|
41
|
+
const grokMedium = resolveModel('xai-grok/grok-4.6-medium');
|
|
42
|
+
expect(grokMedium?.spec.slug).toBe('xai-grok/grok-4.6');
|
|
43
43
|
expect(grokMedium?.effort).toBe('medium');
|
|
44
44
|
|
|
45
45
|
const sonnetMax = resolveModel('anthropic-claude/sonnet-5-max');
|
|
@@ -48,7 +48,7 @@ describe('models registry', () => {
|
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
it('rejects unsupported efforts', () => {
|
|
51
|
-
const grokXhigh = resolveModel('xai-grok/grok-4.
|
|
51
|
+
const grokXhigh = resolveModel('xai-grok/grok-4.6-xhigh');
|
|
52
52
|
expect(grokXhigh).toBeUndefined();
|
|
53
53
|
|
|
54
54
|
const gptOssHigh = resolveModel('google-antigravity/gpt-oss-120b-medium-high');
|
|
@@ -69,27 +69,27 @@ describe('models registry', () => {
|
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
it('computes backendModelId correctly for agy vs others', () => {
|
|
72
|
-
const gemini = resolveModel('google-antigravity/gemini-3.
|
|
72
|
+
const gemini = resolveModel('google-antigravity/gemini-3.7-flash');
|
|
73
73
|
if (!gemini) throw new Error('gemini resolution failed');
|
|
74
|
-
expect(backendModelId(gemini)).toBe('gemini-3.
|
|
74
|
+
expect(backendModelId(gemini)).toBe('gemini-3.7-flash-high');
|
|
75
75
|
|
|
76
|
-
const geminiLow = resolveModel('google-antigravity/gemini-3.
|
|
76
|
+
const geminiLow = resolveModel('google-antigravity/gemini-3.7-flash-low');
|
|
77
77
|
if (!geminiLow) throw new Error('geminiLow resolution failed');
|
|
78
|
-
expect(backendModelId(geminiLow)).toBe('gemini-3.
|
|
78
|
+
expect(backendModelId(geminiLow)).toBe('gemini-3.7-flash-low');
|
|
79
79
|
|
|
80
|
-
const grok = resolveModel('xai-grok/grok-4.
|
|
80
|
+
const grok = resolveModel('xai-grok/grok-4.6');
|
|
81
81
|
if (!grok) throw new Error('grok resolution failed');
|
|
82
|
-
expect(backendModelId(grok)).toBe('grok-4.
|
|
82
|
+
expect(backendModelId(grok)).toBe('grok-4.6');
|
|
83
83
|
|
|
84
84
|
const sonnet = resolveModel('anthropic-claude/sonnet-5');
|
|
85
85
|
if (!sonnet) throw new Error('sonnet resolution failed');
|
|
86
86
|
expect(backendModelId(sonnet)).toBe('claude-sonnet-5');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it('marks codex, grok, and gemini-3.
|
|
89
|
+
it('marks codex, grok, and gemini-3.7-flash seats as image-gen capable', () => {
|
|
90
90
|
const codex = resolveModel('openai-codex/gpt-5.6-sol');
|
|
91
|
-
const grok = resolveModel('xai-grok/grok-4.
|
|
92
|
-
const gemini = resolveModel('google-antigravity/gemini-3.
|
|
91
|
+
const grok = resolveModel('xai-grok/grok-4.6');
|
|
92
|
+
const gemini = resolveModel('google-antigravity/gemini-3.7-flash');
|
|
93
93
|
const claudeSonnet = resolveModel('anthropic-claude/sonnet-5');
|
|
94
94
|
if (!codex || !grok || !gemini || !claudeSonnet) throw new Error('resolution failed');
|
|
95
95
|
expect(supportsImageGen(codex)).toBe(true);
|
|
@@ -100,9 +100,9 @@ describe('models registry', () => {
|
|
|
100
100
|
|
|
101
101
|
it('lists only image-capable seats when imageOnly', () => {
|
|
102
102
|
const lines = listModelHelpLines({ imageOnly: true }).join('\n');
|
|
103
|
-
expect(lines).toContain('xai-grok/grok-4.
|
|
103
|
+
expect(lines).toContain('xai-grok/grok-4.6');
|
|
104
104
|
expect(lines).toContain('openai-codex/gpt-5.6-sol');
|
|
105
|
-
expect(lines).toContain('google-antigravity/gemini-3.
|
|
105
|
+
expect(lines).toContain('google-antigravity/gemini-3.7-flash');
|
|
106
106
|
expect(lines).not.toContain('anthropic-claude/sonnet-5');
|
|
107
107
|
});
|
|
108
108
|
|
|
@@ -112,9 +112,9 @@ describe('models registry', () => {
|
|
|
112
112
|
const err = formatImageGenModelError('anthropic-claude/sonnet-5', claudeSonnet);
|
|
113
113
|
expect(err).toContain('cannot generate images');
|
|
114
114
|
expect(err).toContain('backend "claude"');
|
|
115
|
-
expect(err).toContain('xai-grok/grok-4.
|
|
115
|
+
expect(err).toContain('xai-grok/grok-4.6');
|
|
116
116
|
expect(err).toContain('openai-codex/gpt-5.6-sol');
|
|
117
|
-
expect(err).toContain('google-antigravity/gemini-3.
|
|
117
|
+
expect(err).toContain('google-antigravity/gemini-3.7-flash');
|
|
118
118
|
const seatsSection = err.slice(err.indexOf('Image-gen seats'));
|
|
119
119
|
expect(seatsSection).not.toContain('anthropic-claude/sonnet-5');
|
|
120
120
|
});
|
package/src/models.ts
CHANGED
|
@@ -26,6 +26,15 @@ export interface ResolvedModel {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export const MODELS: Record<string, ModelSpec> = {
|
|
29
|
+
'xai-grok/grok-4.6': {
|
|
30
|
+
slug: 'xai-grok/grok-4.6',
|
|
31
|
+
backend: 'grok',
|
|
32
|
+
backendModel: 'grok-4.6',
|
|
33
|
+
efforts: ['low', 'medium', 'high'],
|
|
34
|
+
brief: 'xAI Grok 4.6 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight',
|
|
35
|
+
},
|
|
36
|
+
// Both grok tiers stay registered: 4.6 and 4.5 differ in character, not just
|
|
37
|
+
// recency, so this is two seats of one class rather than a superseded pin.
|
|
29
38
|
'xai-grok/grok-4.5': {
|
|
30
39
|
slug: 'xai-grok/grok-4.5',
|
|
31
40
|
backend: 'grok',
|
|
@@ -33,6 +42,17 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
33
42
|
efforts: ['low', 'medium', 'high'],
|
|
34
43
|
brief: 'xAI Grok 4.5 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight',
|
|
35
44
|
},
|
|
45
|
+
'google-antigravity/gemini-3.7-flash': {
|
|
46
|
+
slug: 'google-antigravity/gemini-3.7-flash',
|
|
47
|
+
backend: 'agy',
|
|
48
|
+
backendModel: 'gemini-3.7-flash',
|
|
49
|
+
efforts: ['low', 'medium', 'high'],
|
|
50
|
+
defaultEffort: 'high',
|
|
51
|
+
brief:
|
|
52
|
+
'Google Gemini 3.7 Flash via agy — own Antigravity login; quota shared across all Gemini tiers (not yet itemised per-model, so no exhaustion preflight)',
|
|
53
|
+
},
|
|
54
|
+
// 3.6 stays registered while agy's quota endpoint still stops at 3.6: it is
|
|
55
|
+
// the newest flash tier the exhaustion preflight can actually guard.
|
|
36
56
|
'google-antigravity/gemini-3.6-flash': {
|
|
37
57
|
slug: 'google-antigravity/gemini-3.6-flash',
|
|
38
58
|
backend: 'agy',
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
evaluateAgyPreflight,
|
|
8
8
|
evaluateCodexPreflight,
|
|
9
9
|
evaluateGrokPreflight,
|
|
10
|
+
renderPreflightRefusal,
|
|
10
11
|
} from './quotaPreflight.ts';
|
|
11
12
|
|
|
12
13
|
test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', () => {
|
|
@@ -28,6 +29,7 @@ test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', ()
|
|
|
28
29
|
|
|
29
30
|
assert.deepEqual(result, {
|
|
30
31
|
ok: false,
|
|
32
|
+
kind: 'quota',
|
|
31
33
|
message: 'agy model "Gemini 3.5 Flash (High)" is quota-exhausted',
|
|
32
34
|
resetAt: '2024-01-02T12:00:00Z',
|
|
33
35
|
});
|
|
@@ -127,6 +129,7 @@ test('evaluateCodexPreflight: limitReached returns ok:false', () => {
|
|
|
127
129
|
|
|
128
130
|
assert.deepEqual(result, {
|
|
129
131
|
ok: false,
|
|
132
|
+
kind: 'quota',
|
|
130
133
|
message: 'codex quota limit reached',
|
|
131
134
|
resetAt: '2024-01-01T17:00:00Z',
|
|
132
135
|
});
|
|
@@ -199,6 +202,7 @@ test('evaluateGrokPreflight: usedPercent at 100 refuses with resetAt', () => {
|
|
|
199
202
|
|
|
200
203
|
assert.deepEqual(result, {
|
|
201
204
|
ok: false,
|
|
205
|
+
kind: 'quota',
|
|
202
206
|
message: 'grok credit quota exhausted',
|
|
203
207
|
resetAt: '2024-01-08T00:00:00Z',
|
|
204
208
|
});
|
|
@@ -221,3 +225,27 @@ test('evaluateGrokPreflight: healthy (17% used) returns ok:true', () => {
|
|
|
221
225
|
|
|
222
226
|
assert.deepEqual(result, { ok: true });
|
|
223
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,11 +1,17 @@
|
|
|
1
1
|
import { type AgyQuotaSnapshot, fetchAgyQuota, findModelQuota } from '@aibridge/driver-agy';
|
|
2
2
|
import { type CodexQuotaSnapshot, fetchCodexQuota } from '@aibridge/driver-codex';
|
|
3
3
|
import { fetchGrokQuota, type GrokQuotaSnapshot } from '@aibridge/driver-grok';
|
|
4
|
+
import { isAuthExpired } from '@aibridge/proc';
|
|
4
5
|
import { backendModelId, type ResolvedModel } from './models.ts';
|
|
5
6
|
|
|
6
7
|
export type PreflightVerdict =
|
|
7
8
|
| { readonly ok: true; readonly warning?: string }
|
|
8
|
-
| {
|
|
9
|
+
| {
|
|
10
|
+
readonly ok: false;
|
|
11
|
+
readonly kind: 'auth' | 'quota';
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly resetAt: string | undefined;
|
|
14
|
+
};
|
|
9
15
|
|
|
10
16
|
export function evaluateAgyPreflight(
|
|
11
17
|
snapshot: AgyQuotaSnapshot,
|
|
@@ -32,7 +38,12 @@ export function evaluateAgyPreflight(
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
}
|
|
35
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
kind: 'quota',
|
|
44
|
+
message: `agy model "${backendModel}" is quota-exhausted`,
|
|
45
|
+
resetAt,
|
|
46
|
+
};
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
return { ok: true };
|
|
@@ -41,12 +52,17 @@ export function evaluateAgyPreflight(
|
|
|
41
52
|
export function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): PreflightVerdict {
|
|
42
53
|
if (snapshot.limitReached) {
|
|
43
54
|
const resetAt = snapshot.windows.find(w => w.resetAt)?.resetAt;
|
|
44
|
-
return { ok: false, message: 'codex quota limit reached', resetAt };
|
|
55
|
+
return { ok: false, kind: 'quota', message: 'codex quota limit reached', resetAt };
|
|
45
56
|
}
|
|
46
57
|
|
|
47
58
|
const exhaustedWindow = snapshot.windows.find(w => w.usedPercent >= 100);
|
|
48
59
|
if (exhaustedWindow) {
|
|
49
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
kind: 'quota',
|
|
63
|
+
message: 'codex quota limit reached',
|
|
64
|
+
resetAt: exhaustedWindow.resetAt,
|
|
65
|
+
};
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
return { ok: true };
|
|
@@ -56,6 +72,7 @@ export function evaluateGrokPreflight(snapshot: GrokQuotaSnapshot): PreflightVer
|
|
|
56
72
|
if (snapshot.usedPercent !== undefined && snapshot.usedPercent >= 100) {
|
|
57
73
|
return {
|
|
58
74
|
ok: false,
|
|
75
|
+
kind: 'quota',
|
|
59
76
|
message: 'grok credit quota exhausted',
|
|
60
77
|
resetAt: snapshot.periodEnd,
|
|
61
78
|
};
|
|
@@ -81,10 +98,9 @@ export async function preflightModel(resolved: ResolvedModel): Promise<Preflight
|
|
|
81
98
|
const snapshot = await fetchAgyQuota();
|
|
82
99
|
return evaluateAgyPreflight(snapshot, backendModelId(resolved));
|
|
83
100
|
} catch (err) {
|
|
84
|
-
|
|
85
|
-
ok:
|
|
86
|
-
|
|
87
|
-
};
|
|
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` };
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
|
|
@@ -93,10 +109,9 @@ export async function preflightCodex(): Promise<PreflightVerdict> {
|
|
|
93
109
|
const snapshot = await fetchCodexQuota();
|
|
94
110
|
return evaluateCodexPreflight(snapshot);
|
|
95
111
|
} catch (err) {
|
|
96
|
-
|
|
97
|
-
ok:
|
|
98
|
-
|
|
99
|
-
};
|
|
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` };
|
|
100
115
|
}
|
|
101
116
|
}
|
|
102
117
|
|
|
@@ -105,10 +120,9 @@ export async function preflightGrok(): Promise<PreflightVerdict> {
|
|
|
105
120
|
const snapshot = await fetchGrokQuota();
|
|
106
121
|
return evaluateGrokPreflight(snapshot);
|
|
107
122
|
} catch (err) {
|
|
108
|
-
|
|
109
|
-
ok:
|
|
110
|
-
|
|
111
|
-
};
|
|
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` };
|
|
112
126
|
}
|
|
113
127
|
}
|
|
114
128
|
|
|
@@ -124,8 +138,11 @@ function formatReset(resetTime: string | undefined): string {
|
|
|
124
138
|
|
|
125
139
|
export function renderPreflightRefusal(
|
|
126
140
|
cmd: string,
|
|
127
|
-
verdict: { message: string; resetAt: string | undefined },
|
|
141
|
+
verdict: { kind: 'auth' | 'quota'; message: string; resetAt: string | undefined },
|
|
128
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
|
+
}
|
|
129
146
|
const resetClause = verdict.resetAt ? ` Resets ${formatReset(verdict.resetAt)}.` : '';
|
|
130
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).`;
|
|
131
148
|
}
|