@aibridge/cli 0.3.0 → 0.4.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 +31 -0
- package/dist/cli.mjs +1 -1
- package/dist/{context-B6I4QI9z.mjs → context-v_ns4n_W.mjs} +169 -21
- package/dist/index.d.mts +3 -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/driver.ts +6 -1
- package/src/drivers.ts +1 -0
- package/src/models.ts +48 -15
- package/src/quotaPreflight.test.ts +46 -1
- package/src/quotaPreflight.ts +29 -0
package/README.md
CHANGED
|
@@ -42,6 +42,7 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.5 "summarize the architect
|
|
|
42
42
|
| `aibridge review --model xai-grok/grok-4.5 --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
43
|
| `aibridge subagent --model xai-grok/grok-4.5 "<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
|
+
| `aibridge models [--json]` | You need the exact facts for every registered model seat (accepted efforts, image format, pinned model ID) |
|
|
45
46
|
| `aibridge quota` | Two-second check of every backend's remaining quota before you pipeline work |
|
|
46
47
|
| `aibridge runs` | Inspect or watch past delegation runs (`~/.aibridge/runs`) |
|
|
47
48
|
|
|
@@ -63,6 +64,36 @@ Plan files — not their contents — travel between stages, so the loop is near
|
|
|
63
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.)
|
|
64
65
|
- **Models are canonical slugs**: `<vendor>-<cli>/<model>[-<effort>]` — e.g. `xai-grok/grok-4.5`, `google-antigravity/gemini-3.6-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.
|
|
65
66
|
|
|
67
|
+
## Tell your agent when to reach for it
|
|
68
|
+
|
|
69
|
+
aibridge doesn't decide when to delegate — your agent does, and left alone it will
|
|
70
|
+
mostly keep the work for itself. Put the routing rule in whatever instructions file
|
|
71
|
+
your agent already reads at the start of every session (`AGENTS.md`, `CLAUDE.md`,
|
|
72
|
+
`.cursorrules`, …). Something like:
|
|
73
|
+
|
|
74
|
+
```markdown
|
|
75
|
+
## Delegation gate — decide before you implement
|
|
76
|
+
|
|
77
|
+
The moment a task becomes implementation you could fully specify, say the call out
|
|
78
|
+
loud — **solo** or **aibridge** — plus one line of why. Delegating is the default.
|
|
79
|
+
Stay solo only when the edit is smaller than the spec would be, or the work needs
|
|
80
|
+
live judgment, your own session's tools, or tight back-and-forth. Never default to
|
|
81
|
+
solo silently.
|
|
82
|
+
|
|
83
|
+
Route by size and risk:
|
|
84
|
+
|
|
85
|
+
- tiny → solo
|
|
86
|
+
- clearly specified and self-contained → `aibridge subagent`
|
|
87
|
+
- large or risky → `aibridge plan` → read and approve the plan file →
|
|
88
|
+
`aibridge implement` → `aibridge review`
|
|
89
|
+
|
|
90
|
+
Delegated work is yours to verify: re-run the real gates before trusting a diff.
|
|
91
|
+
Prefer a reviewer from a different model family than whoever implemented.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Tune the seats and thresholds to your own quotas. The value is that the decision is
|
|
95
|
+
explicit and made *before* the work starts, rather than rationalised afterwards.
|
|
96
|
+
|
|
66
97
|
## Requirements
|
|
67
98
|
|
|
68
99
|
- **Node ≥ 24.11**
|
package/dist/cli.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { fetchClaudeQuota } from "@aibridge/driver-claude";
|
|
|
10
10
|
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
|
+
import { fetchGrokQuota } from "@aibridge/driver-grok";
|
|
13
14
|
import { runCaptured } from "@aibridge/proc";
|
|
14
15
|
import { randomBytes } from "node:crypto";
|
|
15
16
|
//#region src/models.ts
|
|
@@ -23,7 +24,7 @@ const MODELS = {
|
|
|
23
24
|
"medium",
|
|
24
25
|
"high"
|
|
25
26
|
],
|
|
26
|
-
brief: "xAI Grok 4.5 via grok CLI —
|
|
27
|
+
brief: "xAI Grok 4.5 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight"
|
|
27
28
|
},
|
|
28
29
|
"google-antigravity/gemini-3.6-flash": {
|
|
29
30
|
slug: "google-antigravity/gemini-3.6-flash",
|
|
@@ -35,7 +36,15 @@ const MODELS = {
|
|
|
35
36
|
"high"
|
|
36
37
|
],
|
|
37
38
|
defaultEffort: "high",
|
|
38
|
-
brief: "Google Gemini 3.6 Flash via agy —
|
|
39
|
+
brief: "Google Gemini 3.6 Flash via agy — own Antigravity login; quota shared across all Gemini tiers"
|
|
40
|
+
},
|
|
41
|
+
"google-antigravity/gemini-3.1-pro": {
|
|
42
|
+
slug: "google-antigravity/gemini-3.1-pro",
|
|
43
|
+
backend: "agy",
|
|
44
|
+
efforts: ["low", "high"],
|
|
45
|
+
backendModel: "gemini-3.1-pro",
|
|
46
|
+
defaultEffort: "high",
|
|
47
|
+
brief: "Google Gemini 3.1 Pro via agy — own Antigravity login; quota shared across all Gemini tiers"
|
|
39
48
|
},
|
|
40
49
|
"google-antigravity/claude-sonnet-4-6": {
|
|
41
50
|
slug: "google-antigravity/claude-sonnet-4-6",
|
|
@@ -68,12 +77,36 @@ const MODELS = {
|
|
|
68
77
|
"high",
|
|
69
78
|
"xhigh"
|
|
70
79
|
],
|
|
71
|
-
brief: "OpenAI
|
|
80
|
+
brief: "OpenAI gpt-5.6-sol via codex CLI — frontier agentic coding; own ChatGPT login"
|
|
72
81
|
},
|
|
73
|
-
"
|
|
74
|
-
slug: "
|
|
82
|
+
"openai-codex/gpt-5.6-terra": {
|
|
83
|
+
slug: "openai-codex/gpt-5.6-terra",
|
|
84
|
+
backend: "codex",
|
|
85
|
+
backendModel: "gpt-5.6-terra",
|
|
86
|
+
efforts: [
|
|
87
|
+
"low",
|
|
88
|
+
"medium",
|
|
89
|
+
"high",
|
|
90
|
+
"xhigh"
|
|
91
|
+
],
|
|
92
|
+
brief: "OpenAI gpt-5.6-terra via codex CLI — balanced, everyday coding; own ChatGPT login"
|
|
93
|
+
},
|
|
94
|
+
"openai-codex/gpt-5.6-luna": {
|
|
95
|
+
slug: "openai-codex/gpt-5.6-luna",
|
|
96
|
+
backend: "codex",
|
|
97
|
+
backendModel: "gpt-5.6-luna",
|
|
98
|
+
efforts: [
|
|
99
|
+
"low",
|
|
100
|
+
"medium",
|
|
101
|
+
"high",
|
|
102
|
+
"xhigh"
|
|
103
|
+
],
|
|
104
|
+
brief: "OpenAI gpt-5.6-luna via codex CLI — fast and affordable coding; own ChatGPT login"
|
|
105
|
+
},
|
|
106
|
+
"anthropic-claude/fable-5": {
|
|
107
|
+
slug: "anthropic-claude/fable-5",
|
|
75
108
|
backend: "claude",
|
|
76
|
-
backendModel: "claude-
|
|
109
|
+
backendModel: "claude-fable-5",
|
|
77
110
|
efforts: [
|
|
78
111
|
"low",
|
|
79
112
|
"medium",
|
|
@@ -81,12 +114,13 @@ const MODELS = {
|
|
|
81
114
|
"xhigh",
|
|
82
115
|
"max"
|
|
83
116
|
],
|
|
84
|
-
|
|
117
|
+
defaultEffort: "high",
|
|
118
|
+
brief: "Claude Fable 5 via claude CLI — hardest, longest-running work; bills the claude CLI subscription"
|
|
85
119
|
},
|
|
86
120
|
"anthropic-claude/opus-5": {
|
|
87
121
|
slug: "anthropic-claude/opus-5",
|
|
88
122
|
backend: "claude",
|
|
89
|
-
backendModel: "claude-opus-5",
|
|
123
|
+
backendModel: "claude-opus-5[1m]",
|
|
90
124
|
efforts: [
|
|
91
125
|
"low",
|
|
92
126
|
"medium",
|
|
@@ -95,12 +129,12 @@ const MODELS = {
|
|
|
95
129
|
"max"
|
|
96
130
|
],
|
|
97
131
|
defaultEffort: "high",
|
|
98
|
-
brief: "Claude Opus 5 via claude CLI
|
|
132
|
+
brief: "Claude Opus 5, 1M context via claude CLI — everyday complex work; bills the claude CLI subscription"
|
|
99
133
|
},
|
|
100
|
-
"anthropic-claude/
|
|
101
|
-
slug: "anthropic-claude/
|
|
134
|
+
"anthropic-claude/sonnet-5": {
|
|
135
|
+
slug: "anthropic-claude/sonnet-5",
|
|
102
136
|
backend: "claude",
|
|
103
|
-
backendModel: "claude-
|
|
137
|
+
backendModel: "claude-sonnet-5",
|
|
104
138
|
efforts: [
|
|
105
139
|
"low",
|
|
106
140
|
"medium",
|
|
@@ -108,8 +142,20 @@ const MODELS = {
|
|
|
108
142
|
"xhigh",
|
|
109
143
|
"max"
|
|
110
144
|
],
|
|
111
|
-
|
|
112
|
-
|
|
145
|
+
brief: "Claude Sonnet 5 via claude CLI — routine work; bills the claude CLI subscription"
|
|
146
|
+
},
|
|
147
|
+
"anthropic-claude/haiku-4-5": {
|
|
148
|
+
slug: "anthropic-claude/haiku-4-5",
|
|
149
|
+
backend: "claude",
|
|
150
|
+
backendModel: "claude-haiku-4-5-20251001",
|
|
151
|
+
efforts: [
|
|
152
|
+
"low",
|
|
153
|
+
"medium",
|
|
154
|
+
"high",
|
|
155
|
+
"xhigh",
|
|
156
|
+
"max"
|
|
157
|
+
],
|
|
158
|
+
brief: "Claude Haiku 4.5 via claude CLI — quick answers; bills the claude CLI subscription"
|
|
113
159
|
}
|
|
114
160
|
};
|
|
115
161
|
const IMAGE_GEN_FORMATS = /* @__PURE__ */ new Map([
|
|
@@ -209,6 +255,7 @@ const DRIVERS = {
|
|
|
209
255
|
grok: {
|
|
210
256
|
probe: () => grok.probe(),
|
|
211
257
|
run: (task) => grok.run(task),
|
|
258
|
+
quota: () => grok.fetchGrokQuota(),
|
|
212
259
|
generateImage: (req) => grok.generateImage(req)
|
|
213
260
|
},
|
|
214
261
|
codex: {
|
|
@@ -488,8 +535,17 @@ function evaluateCodexPreflight(snapshot) {
|
|
|
488
535
|
};
|
|
489
536
|
return { ok: true };
|
|
490
537
|
}
|
|
538
|
+
function evaluateGrokPreflight(snapshot) {
|
|
539
|
+
if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
|
|
540
|
+
ok: false,
|
|
541
|
+
message: "grok credit quota exhausted",
|
|
542
|
+
resetAt: snapshot.periodEnd
|
|
543
|
+
};
|
|
544
|
+
return { ok: true };
|
|
545
|
+
}
|
|
491
546
|
async function preflightModel(resolved) {
|
|
492
547
|
if (resolved.spec.backend === "codex") return preflightCodex();
|
|
548
|
+
if (resolved.spec.backend === "grok") return preflightGrok();
|
|
493
549
|
if (resolved.spec.backend !== "agy") return { ok: true };
|
|
494
550
|
try {
|
|
495
551
|
return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
|
|
@@ -510,6 +566,16 @@ async function preflightCodex() {
|
|
|
510
566
|
};
|
|
511
567
|
}
|
|
512
568
|
}
|
|
569
|
+
async function preflightGrok() {
|
|
570
|
+
try {
|
|
571
|
+
return evaluateGrokPreflight(await fetchGrokQuota());
|
|
572
|
+
} catch (err) {
|
|
573
|
+
return {
|
|
574
|
+
ok: true,
|
|
575
|
+
warning: `quota preflight failed (${err.message}); proceeding`
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
513
579
|
function formatReset$1(resetTime) {
|
|
514
580
|
if (!resetTime) return "-";
|
|
515
581
|
const ms = new Date(resetTime).getTime() - Date.now();
|
|
@@ -744,6 +810,70 @@ const implement = buildCommand({
|
|
|
744
810
|
}
|
|
745
811
|
});
|
|
746
812
|
//#endregion
|
|
813
|
+
//#region src/commands/models/impl.ts
|
|
814
|
+
const BACKEND_DISPLAY_NAMES = {
|
|
815
|
+
grok: "grok (Grok CLI)",
|
|
816
|
+
agy: "agy (Antigravity)",
|
|
817
|
+
codex: "codex (Codex CLI)",
|
|
818
|
+
claude: "claude (Claude Code CLI)"
|
|
819
|
+
};
|
|
820
|
+
function modelsImpl(flags) {
|
|
821
|
+
const specs = Object.values(MODELS);
|
|
822
|
+
if (flags.json) {
|
|
823
|
+
const jsonOutput = specs.map((spec) => ({
|
|
824
|
+
slug: spec.slug,
|
|
825
|
+
backend: spec.backend,
|
|
826
|
+
backendModel: spec.backendModel,
|
|
827
|
+
efforts: spec.efforts ? [...spec.efforts] : [],
|
|
828
|
+
defaultEffort: spec.defaultEffort ?? null,
|
|
829
|
+
image: imageFormatFor({
|
|
830
|
+
spec,
|
|
831
|
+
effort: void 0
|
|
832
|
+
}) ?? null,
|
|
833
|
+
brief: spec.brief
|
|
834
|
+
}));
|
|
835
|
+
this.process.stdout.write(`${JSON.stringify(jsonOutput)}\n`);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const backends = [];
|
|
839
|
+
for (const spec of specs) if (!backends.includes(spec.backend)) backends.push(spec.backend);
|
|
840
|
+
let firstBackend = true;
|
|
841
|
+
for (const backend of backends) {
|
|
842
|
+
if (!firstBackend) this.process.stdout.write("\n");
|
|
843
|
+
firstBackend = false;
|
|
844
|
+
this.process.stdout.write(`=== ${BACKEND_DISPLAY_NAMES[backend]} ===\n`);
|
|
845
|
+
const backendSpecs = specs.filter((spec) => spec.backend === backend);
|
|
846
|
+
for (const spec of backendSpecs) {
|
|
847
|
+
this.process.stdout.write(` ${spec.slug}\n`);
|
|
848
|
+
const segments = [];
|
|
849
|
+
if (spec.efforts) {
|
|
850
|
+
const formattedEfforts = spec.efforts.map((e) => e === spec.defaultEffort ? `${e}*` : e).join(" | ");
|
|
851
|
+
segments.push(`efforts: ${formattedEfforts}`);
|
|
852
|
+
}
|
|
853
|
+
const img = imageFormatFor({
|
|
854
|
+
spec,
|
|
855
|
+
effort: void 0
|
|
856
|
+
});
|
|
857
|
+
segments.push(`image: ${img ?? "—"}`);
|
|
858
|
+
segments.push(`id: ${spec.backendModel}`);
|
|
859
|
+
this.process.stdout.write(` ${segments.join(" · ")}\n`);
|
|
860
|
+
this.process.stdout.write(` ${spec.brief}\n`);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (specs.some((spec) => spec.defaultEffort !== void 0)) this.process.stdout.write("\n* = effort used when the slug has no -<effort> suffix\n");
|
|
864
|
+
}
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/commands/models/command.ts
|
|
867
|
+
const models = buildCommand({
|
|
868
|
+
func: modelsImpl,
|
|
869
|
+
parameters: { flags: { json: {
|
|
870
|
+
kind: "boolean",
|
|
871
|
+
withNegated: false,
|
|
872
|
+
brief: "Emit the registry as JSON"
|
|
873
|
+
} } },
|
|
874
|
+
docs: { brief: "List every model seat in the registry (slug, efforts, image format)" }
|
|
875
|
+
});
|
|
876
|
+
//#endregion
|
|
747
877
|
//#region src/commands/plan/impl.ts
|
|
748
878
|
function countOpenQuestions(markdown) {
|
|
749
879
|
const headingIdx = markdown.search(/^## Open questions[ \t]*$/m);
|
|
@@ -905,6 +1035,17 @@ function formatReset(resetTime) {
|
|
|
905
1035
|
const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
|
|
906
1036
|
return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
|
|
907
1037
|
}
|
|
1038
|
+
function renderGrok(ctx, snapshot) {
|
|
1039
|
+
ctx.process.stdout.write("=== grok (xAI) — used this period ===\n");
|
|
1040
|
+
ctx.process.stdout.write(`${"PERIOD".padEnd(10)} ${"USED".padEnd(10)} RESET\n`);
|
|
1041
|
+
const usedPctStr = snapshot.usedPercent !== void 0 ? `${snapshot.usedPercent}%` : "?";
|
|
1042
|
+
const periodStr = snapshot.periodType ?? "-";
|
|
1043
|
+
ctx.process.stdout.write(`${periodStr.padEnd(10)} ${usedPctStr.padEnd(10)} ${formatReset(snapshot.periodEnd)}\n`);
|
|
1044
|
+
if (snapshot.products.length > 0) {
|
|
1045
|
+
const prods = snapshot.products.map((p) => `${p.product} ${p.usedPercent}%`).join(" · ");
|
|
1046
|
+
ctx.process.stdout.write(` ${prods}\n`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
908
1049
|
function renderAgy(ctx, snapshot) {
|
|
909
1050
|
ctx.process.stdout.write("=== agy (Antigravity) — remaining per model group ===\n");
|
|
910
1051
|
for (const group of snapshot.groups) {
|
|
@@ -934,14 +1075,16 @@ function renderSection(ctx, result, title, render) {
|
|
|
934
1075
|
else ctx.process.stdout.write(`=== ${title} ===\nunavailable: ${result.reason.message}\n`);
|
|
935
1076
|
}
|
|
936
1077
|
async function quotaImpl(flags) {
|
|
937
|
-
const [agy, codex, claude] = await Promise.allSettled([
|
|
1078
|
+
const [grok, agy, codex, claude] = await Promise.allSettled([
|
|
1079
|
+
fetchGrokQuota(),
|
|
938
1080
|
fetchAgyQuota(),
|
|
939
1081
|
fetchCodexQuota(),
|
|
940
1082
|
fetchClaudeQuota()
|
|
941
1083
|
]);
|
|
942
|
-
const allFailed = agy.status === "rejected" && codex.status === "rejected" && claude.status === "rejected";
|
|
1084
|
+
const allFailed = grok.status === "rejected" && agy.status === "rejected" && codex.status === "rejected" && claude.status === "rejected";
|
|
943
1085
|
if (flags.json) {
|
|
944
1086
|
this.process.stdout.write(`${JSON.stringify({
|
|
1087
|
+
grok: grok.status === "fulfilled" ? grok.value : { error: String(grok.reason) },
|
|
945
1088
|
agy: agy.status === "fulfilled" ? agy.value : { error: String(agy.reason) },
|
|
946
1089
|
codex: codex.status === "fulfilled" ? codex.value : { error: String(codex.reason) },
|
|
947
1090
|
claude: claude.status === "fulfilled" ? claude.value : { error: String(claude.reason) }
|
|
@@ -949,6 +1092,8 @@ async function quotaImpl(flags) {
|
|
|
949
1092
|
if (allFailed) this.process.exitCode = 1;
|
|
950
1093
|
return;
|
|
951
1094
|
}
|
|
1095
|
+
renderSection(this, grok, "grok (xAI)", renderGrok);
|
|
1096
|
+
this.process.stdout.write("\n");
|
|
952
1097
|
renderSection(this, agy, "agy (Antigravity)", renderAgy);
|
|
953
1098
|
this.process.stdout.write("\n");
|
|
954
1099
|
renderSection(this, codex, "codex (ChatGPT)", renderCodex);
|
|
@@ -964,8 +1109,10 @@ const quota = buildCommand({
|
|
|
964
1109
|
brief: "Emit the raw snapshot as JSON"
|
|
965
1110
|
} } },
|
|
966
1111
|
docs: {
|
|
967
|
-
brief: "Show agy / codex / claude quota with reset times",
|
|
1112
|
+
brief: "Show grok / agy / codex / claude quota with reset times",
|
|
968
1113
|
fullDescription: [
|
|
1114
|
+
"grok: reads ~/.grok/auth.json and asks the xAI billing endpoint for the",
|
|
1115
|
+
"weekly credit usage percentage and per-product split.",
|
|
969
1116
|
"agy: reads its cached OAuth token (~/.gemini/antigravity-cli/) and asks the",
|
|
970
1117
|
"Cloud Code API for per-model remaining quota. EXHAUSTED means agy turns on",
|
|
971
1118
|
"that model fail with an empty answer until the reset time.",
|
|
@@ -1390,8 +1537,8 @@ const fullDescription = [
|
|
|
1390
1537
|
"",
|
|
1391
1538
|
"Available models (canonical slug):",
|
|
1392
1539
|
...listModelHelpLines(),
|
|
1393
|
-
"Recommended first choice: xai-grok/grok-4.5
|
|
1394
|
-
"
|
|
1540
|
+
"Recommended first choice: xai-grok/grok-4.5. Whichever seat runs on the same provider as the",
|
|
1541
|
+
"agent you orchestrate from is your last resort — it spends the pool you are already burning."
|
|
1395
1542
|
].join("\n");
|
|
1396
1543
|
const subagent = buildCommand({
|
|
1397
1544
|
func: subagent$1,
|
|
@@ -1465,9 +1612,10 @@ const app = buildApplication(buildRouteMap({
|
|
|
1465
1612
|
subagent,
|
|
1466
1613
|
"image-gen": imageGen,
|
|
1467
1614
|
runs,
|
|
1468
|
-
quota
|
|
1615
|
+
quota,
|
|
1616
|
+
models
|
|
1469
1617
|
},
|
|
1470
|
-
docs: { brief: "Bridge tasks to
|
|
1618
|
+
docs: { brief: "Bridge tasks to the other AI CLIs on this machine — a plan → implement → review workflow, task delegation, and image generation (codex / agy / grok seats)." }
|
|
1471
1619
|
}), {
|
|
1472
1620
|
name: "aibridge",
|
|
1473
1621
|
versionInfo: { currentVersion: version },
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,7 @@ import { CommandContext } from "@stricli/core";
|
|
|
2
2
|
import { AgyQuotaSnapshot } from "@aibridge/driver-agy";
|
|
3
3
|
import { ClaudeQuotaSnapshot } from "@aibridge/driver-claude";
|
|
4
4
|
import { CodexQuotaSnapshot } from "@aibridge/driver-codex";
|
|
5
|
+
import { GrokQuotaSnapshot } from "@aibridge/driver-grok";
|
|
5
6
|
//#region src/context.d.ts
|
|
6
7
|
interface LocalContext extends CommandContext {
|
|
7
8
|
/** Full Node process — satisfies stricli WritableStreams + exitCode/env/cwd used by impls. */
|
|
@@ -21,7 +22,7 @@ declare function runCli(ctx: LocalContext, argv: readonly string[]): Promise<voi
|
|
|
21
22
|
* Models are registered by canonical, provider-qualified slug —
|
|
22
23
|
* `<vendor>-<cli>/<model>[-<effort>]`, e.g. `openai-codex/gpt-5.6-sol-high`.
|
|
23
24
|
* Canonical slugs only — no short aliases, by design. That holds on both sides:
|
|
24
|
-
* `backendModel` is a pinned model id (`claude-
|
|
25
|
+
* `backendModel` is a pinned model id (`claude-sonnet-5`), never a moving vendor
|
|
25
26
|
* alias (`opus`), so a seat never silently changes model under you.
|
|
26
27
|
*/
|
|
27
28
|
type Backend = 'agy' | 'claude' | 'codex' | 'grok';
|
|
@@ -77,7 +78,7 @@ type DelegationResult = {
|
|
|
77
78
|
readonly message: string;
|
|
78
79
|
readonly exitCode: number | null;
|
|
79
80
|
};
|
|
80
|
-
type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot;
|
|
81
|
+
type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot | GrokQuotaSnapshot;
|
|
81
82
|
interface ImageGenRequest {
|
|
82
83
|
readonly prompt: string;
|
|
83
84
|
readonly workDir: 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-v_ns4n_W.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.4.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-codex": "0.
|
|
42
|
-
"@aibridge/driver-claude": "0.
|
|
38
|
+
"@aibridge/proc": "0.4.0",
|
|
39
|
+
"@aibridge/driver-grok": "0.4.0",
|
|
40
|
+
"@aibridge/driver-agy": "0.4.0",
|
|
41
|
+
"@aibridge/driver-codex": "0.4.0",
|
|
42
|
+
"@aibridge/driver-claude": "0.4.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"tsdown": "0.22.14"
|
package/src/app.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { buildApplication, buildRouteMap, run } from '@stricli/core';
|
|
3
3
|
import { imageGen } from './commands/image-gen/command.ts';
|
|
4
4
|
import { implement } from './commands/implement/command.ts';
|
|
5
|
+
import { models } from './commands/models/command.ts';
|
|
5
6
|
import { plan } from './commands/plan/command.ts';
|
|
6
7
|
import { quota } from './commands/quota/command.ts';
|
|
7
8
|
import { review } from './commands/review/command.ts';
|
|
@@ -14,7 +15,7 @@ const require = createRequire(import.meta.url);
|
|
|
14
15
|
const { version } = require('../package.json') as { version: string };
|
|
15
16
|
|
|
16
17
|
const BRIEF =
|
|
17
|
-
'Bridge tasks to
|
|
18
|
+
'Bridge tasks to the other AI CLIs on this machine — a plan → implement → review workflow, task delegation, and image generation (codex / agy / grok seats).';
|
|
18
19
|
|
|
19
20
|
const routes = buildRouteMap({
|
|
20
21
|
routes: {
|
|
@@ -25,6 +26,7 @@ const routes = buildRouteMap({
|
|
|
25
26
|
'image-gen': imageGen,
|
|
26
27
|
runs,
|
|
27
28
|
quota,
|
|
29
|
+
models,
|
|
28
30
|
},
|
|
29
31
|
docs: {
|
|
30
32
|
brief: BRIEF,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { buildCommand } from '@stricli/core';
|
|
2
|
+
import modelsImpl from './impl.ts';
|
|
3
|
+
|
|
4
|
+
export const models = buildCommand({
|
|
5
|
+
func: modelsImpl,
|
|
6
|
+
parameters: {
|
|
7
|
+
flags: {
|
|
8
|
+
json: {
|
|
9
|
+
kind: 'boolean',
|
|
10
|
+
withNegated: false,
|
|
11
|
+
brief: 'Emit the registry as JSON',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
docs: {
|
|
16
|
+
brief: 'List every model seat in the registry (slug, efforts, image format)',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { LocalContext } from '../../context.ts';
|
|
3
|
+
import { MODELS } from '../../models.ts';
|
|
4
|
+
import modelsImpl from './impl.ts';
|
|
5
|
+
|
|
6
|
+
function createTestContext() {
|
|
7
|
+
let stdoutText = '';
|
|
8
|
+
const fakeProcess = {
|
|
9
|
+
stdout: {
|
|
10
|
+
write(chunk: string | Uint8Array) {
|
|
11
|
+
stdoutText += chunk.toString();
|
|
12
|
+
return true;
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
stderr: {
|
|
16
|
+
write() {
|
|
17
|
+
return true;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
exitCode: 0,
|
|
21
|
+
} as unknown as NodeJS.Process;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
ctx: { process: fakeProcess } as LocalContext,
|
|
25
|
+
getStdout: () => stdoutText,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('modelsImpl', () => {
|
|
30
|
+
it('--json emits parseable JSON with one entry per key of MODELS, with seven documented fields', () => {
|
|
31
|
+
const { ctx, getStdout } = createTestContext();
|
|
32
|
+
modelsImpl.call(ctx, { json: true });
|
|
33
|
+
|
|
34
|
+
const raw = getStdout();
|
|
35
|
+
const data = JSON.parse(raw);
|
|
36
|
+
|
|
37
|
+
expect(Array.isArray(data)).toBe(true);
|
|
38
|
+
expect(data.length).toBe(Object.keys(MODELS).length);
|
|
39
|
+
|
|
40
|
+
for (const item of data) {
|
|
41
|
+
expect(item).toHaveProperty('slug');
|
|
42
|
+
expect(item).toHaveProperty('backend');
|
|
43
|
+
expect(item).toHaveProperty('backendModel');
|
|
44
|
+
expect(item).toHaveProperty('efforts');
|
|
45
|
+
expect(item).toHaveProperty('defaultEffort');
|
|
46
|
+
expect(item).toHaveProperty('image');
|
|
47
|
+
expect(item).toHaveProperty('brief');
|
|
48
|
+
expect(Object.keys(item)).toHaveLength(7);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reports efforts: ["low", "high"] and defaultEffort: "high" for gemini-3.1-pro in JSON', () => {
|
|
53
|
+
const { ctx, getStdout } = createTestContext();
|
|
54
|
+
modelsImpl.call(ctx, { json: true });
|
|
55
|
+
|
|
56
|
+
const data = JSON.parse(getStdout());
|
|
57
|
+
const geminiPro = data.find(
|
|
58
|
+
(item: { slug: string }) => item.slug === 'google-antigravity/gemini-3.1-pro',
|
|
59
|
+
);
|
|
60
|
+
expect(geminiPro).toBeDefined();
|
|
61
|
+
expect(geminiPro.efforts).toEqual(['low', 'high']);
|
|
62
|
+
expect(geminiPro.defaultEffort).toBe('high');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('reports backendModel and image correctly for opus-5 and gpt-5.6-sol in JSON', () => {
|
|
66
|
+
const { ctx, getStdout } = createTestContext();
|
|
67
|
+
modelsImpl.call(ctx, { json: true });
|
|
68
|
+
|
|
69
|
+
const data = JSON.parse(getStdout());
|
|
70
|
+
const opus = data.find((item: { slug: string }) => item.slug === 'anthropic-claude/opus-5');
|
|
71
|
+
expect(opus).toBeDefined();
|
|
72
|
+
expect(opus.backendModel).toBe('claude-opus-5[1m]');
|
|
73
|
+
expect(opus.image).toBeNull();
|
|
74
|
+
|
|
75
|
+
const sol = data.find((item: { slug: string }) => item.slug === 'openai-codex/gpt-5.6-sol');
|
|
76
|
+
expect(sol).toBeDefined();
|
|
77
|
+
expect(sol.image).toBe('png');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('human output (no --json) contains every slug in MODELS', () => {
|
|
81
|
+
const { ctx, getStdout } = createTestContext();
|
|
82
|
+
modelsImpl.call(ctx, { json: false });
|
|
83
|
+
|
|
84
|
+
const output = getStdout();
|
|
85
|
+
for (const slug of Object.keys(MODELS)) {
|
|
86
|
+
expect(output).toContain(slug);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { LocalContext } from '../../context.ts';
|
|
2
|
+
import { type Backend, imageFormatFor, MODELS } from '../../models.ts';
|
|
3
|
+
|
|
4
|
+
export interface ModelsFlags {
|
|
5
|
+
readonly json: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const BACKEND_DISPLAY_NAMES: Record<Backend, string> = {
|
|
9
|
+
grok: 'grok (Grok CLI)',
|
|
10
|
+
agy: 'agy (Antigravity)',
|
|
11
|
+
codex: 'codex (Codex CLI)',
|
|
12
|
+
claude: 'claude (Claude Code CLI)',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function modelsImpl(this: LocalContext, flags: ModelsFlags): void {
|
|
16
|
+
const specs = Object.values(MODELS);
|
|
17
|
+
|
|
18
|
+
if (flags.json) {
|
|
19
|
+
const jsonOutput = specs.map(spec => ({
|
|
20
|
+
slug: spec.slug,
|
|
21
|
+
backend: spec.backend,
|
|
22
|
+
backendModel: spec.backendModel,
|
|
23
|
+
efforts: spec.efforts ? [...spec.efforts] : [],
|
|
24
|
+
defaultEffort: spec.defaultEffort ?? null,
|
|
25
|
+
image: imageFormatFor({ spec, effort: undefined }) ?? null,
|
|
26
|
+
brief: spec.brief,
|
|
27
|
+
}));
|
|
28
|
+
this.process.stdout.write(`${JSON.stringify(jsonOutput)}\n`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const backends: Backend[] = [];
|
|
33
|
+
for (const spec of specs) {
|
|
34
|
+
if (!backends.includes(spec.backend)) {
|
|
35
|
+
backends.push(spec.backend);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let firstBackend = true;
|
|
40
|
+
for (const backend of backends) {
|
|
41
|
+
if (!firstBackend) {
|
|
42
|
+
this.process.stdout.write('\n');
|
|
43
|
+
}
|
|
44
|
+
firstBackend = false;
|
|
45
|
+
|
|
46
|
+
this.process.stdout.write(`=== ${BACKEND_DISPLAY_NAMES[backend]} ===\n`);
|
|
47
|
+
const backendSpecs = specs.filter(spec => spec.backend === backend);
|
|
48
|
+
for (const spec of backendSpecs) {
|
|
49
|
+
this.process.stdout.write(` ${spec.slug}\n`);
|
|
50
|
+
|
|
51
|
+
const segments: string[] = [];
|
|
52
|
+
if (spec.efforts) {
|
|
53
|
+
const formattedEfforts = spec.efforts
|
|
54
|
+
.map(e => (e === spec.defaultEffort ? `${e}*` : e))
|
|
55
|
+
.join(' | ');
|
|
56
|
+
segments.push(`efforts: ${formattedEfforts}`);
|
|
57
|
+
}
|
|
58
|
+
const img = imageFormatFor({ spec, effort: undefined });
|
|
59
|
+
segments.push(`image: ${img ?? '—'}`);
|
|
60
|
+
segments.push(`id: ${spec.backendModel}`);
|
|
61
|
+
|
|
62
|
+
this.process.stdout.write(` ${segments.join(' · ')}\n`);
|
|
63
|
+
this.process.stdout.write(` ${spec.brief}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hasDefaultEffort = specs.some(spec => spec.defaultEffort !== undefined);
|
|
68
|
+
if (hasDefaultEffort) {
|
|
69
|
+
this.process.stdout.write('\n* = effort used when the slug has no -<effort> suffix\n');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -2,6 +2,8 @@ import { buildCommand } from '@stricli/core';
|
|
|
2
2
|
import quotaImpl from './impl.ts';
|
|
3
3
|
|
|
4
4
|
const fullDescription = [
|
|
5
|
+
'grok: reads ~/.grok/auth.json and asks the xAI billing endpoint for the',
|
|
6
|
+
'weekly credit usage percentage and per-product split.',
|
|
5
7
|
'agy: reads its cached OAuth token (~/.gemini/antigravity-cli/) and asks the',
|
|
6
8
|
'Cloud Code API for per-model remaining quota. EXHAUSTED means agy turns on',
|
|
7
9
|
'that model fail with an empty answer until the reset time.',
|
|
@@ -24,7 +26,7 @@ export const quota = buildCommand({
|
|
|
24
26
|
},
|
|
25
27
|
},
|
|
26
28
|
docs: {
|
|
27
|
-
brief: 'Show agy / codex / claude quota with reset times',
|
|
29
|
+
brief: 'Show grok / agy / codex / claude quota with reset times',
|
|
28
30
|
fullDescription,
|
|
29
31
|
},
|
|
30
32
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AgyQuotaSnapshot, fetchAgyQuota } from '@aibridge/driver-agy';
|
|
2
2
|
import { type ClaudeQuotaSnapshot, fetchClaudeQuota } from '@aibridge/driver-claude';
|
|
3
3
|
import { type CodexQuotaSnapshot, fetchCodexQuota } from '@aibridge/driver-codex';
|
|
4
|
+
import { fetchGrokQuota, type GrokQuotaSnapshot } from '@aibridge/driver-grok';
|
|
4
5
|
import type { LocalContext } from '../../context.ts';
|
|
5
6
|
|
|
6
7
|
export interface QuotaFlags {
|
|
@@ -17,6 +18,20 @@ function formatReset(resetTime: string | undefined): string {
|
|
|
17
18
|
return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function renderGrok(ctx: LocalContext, snapshot: GrokQuotaSnapshot): void {
|
|
22
|
+
ctx.process.stdout.write('=== grok (xAI) — used this period ===\n');
|
|
23
|
+
ctx.process.stdout.write(`${'PERIOD'.padEnd(10)} ${'USED'.padEnd(10)} RESET\n`);
|
|
24
|
+
const usedPctStr = snapshot.usedPercent !== undefined ? `${snapshot.usedPercent}%` : '?';
|
|
25
|
+
const periodStr = snapshot.periodType ?? '-';
|
|
26
|
+
ctx.process.stdout.write(
|
|
27
|
+
`${periodStr.padEnd(10)} ${usedPctStr.padEnd(10)} ${formatReset(snapshot.periodEnd)}\n`,
|
|
28
|
+
);
|
|
29
|
+
if (snapshot.products.length > 0) {
|
|
30
|
+
const prods = snapshot.products.map(p => `${p.product} ${p.usedPercent}%`).join(' · ');
|
|
31
|
+
ctx.process.stdout.write(` ${prods}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
function renderAgy(ctx: LocalContext, snapshot: AgyQuotaSnapshot): void {
|
|
21
36
|
ctx.process.stdout.write('=== agy (Antigravity) — remaining per model group ===\n');
|
|
22
37
|
for (const group of snapshot.groups) {
|
|
@@ -75,19 +90,24 @@ function renderSection<T>(
|
|
|
75
90
|
}
|
|
76
91
|
|
|
77
92
|
export default async function quotaImpl(this: LocalContext, flags: QuotaFlags): Promise<void> {
|
|
78
|
-
const [agy, codex, claude] = await Promise.allSettled([
|
|
93
|
+
const [grok, agy, codex, claude] = await Promise.allSettled([
|
|
94
|
+
fetchGrokQuota(),
|
|
79
95
|
fetchAgyQuota(),
|
|
80
96
|
fetchCodexQuota(),
|
|
81
97
|
fetchClaudeQuota(),
|
|
82
98
|
]);
|
|
83
99
|
|
|
84
100
|
const allFailed =
|
|
85
|
-
|
|
101
|
+
grok.status === 'rejected' &&
|
|
102
|
+
agy.status === 'rejected' &&
|
|
103
|
+
codex.status === 'rejected' &&
|
|
104
|
+
claude.status === 'rejected';
|
|
86
105
|
|
|
87
106
|
if (flags.json) {
|
|
88
107
|
this.process.stdout.write(
|
|
89
108
|
`${JSON.stringify(
|
|
90
109
|
{
|
|
110
|
+
grok: grok.status === 'fulfilled' ? grok.value : { error: String(grok.reason) },
|
|
91
111
|
agy: agy.status === 'fulfilled' ? agy.value : { error: String(agy.reason) },
|
|
92
112
|
codex: codex.status === 'fulfilled' ? codex.value : { error: String(codex.reason) },
|
|
93
113
|
claude: claude.status === 'fulfilled' ? claude.value : { error: String(claude.reason) },
|
|
@@ -100,6 +120,8 @@ export default async function quotaImpl(this: LocalContext, flags: QuotaFlags):
|
|
|
100
120
|
return;
|
|
101
121
|
}
|
|
102
122
|
|
|
123
|
+
renderSection(this, grok, 'grok (xAI)', renderGrok);
|
|
124
|
+
this.process.stdout.write('\n');
|
|
103
125
|
renderSection(this, agy, 'agy (Antigravity)', renderAgy);
|
|
104
126
|
this.process.stdout.write('\n');
|
|
105
127
|
renderSection(this, codex, 'codex (ChatGPT)', renderCodex);
|
|
@@ -8,8 +8,8 @@ const fullDescription = [
|
|
|
8
8
|
'',
|
|
9
9
|
'Available models (canonical slug):',
|
|
10
10
|
...listModelHelpLines(),
|
|
11
|
-
'Recommended first choice: xai-grok/grok-4.5
|
|
12
|
-
'
|
|
11
|
+
'Recommended first choice: xai-grok/grok-4.5. Whichever seat runs on the same provider as the',
|
|
12
|
+
'agent you orchestrate from is your last resort — it spends the pool you are already burning.',
|
|
13
13
|
].join('\n');
|
|
14
14
|
|
|
15
15
|
export const subagent = buildCommand({
|
package/src/driver.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgyQuotaSnapshot } from '@aibridge/driver-agy';
|
|
2
2
|
import type { ClaudeQuotaSnapshot } from '@aibridge/driver-claude';
|
|
3
3
|
import type { CodexQuotaSnapshot } from '@aibridge/driver-codex';
|
|
4
|
+
import type { GrokQuotaSnapshot } from '@aibridge/driver-grok';
|
|
4
5
|
import type { Effort } from './models.ts';
|
|
5
6
|
|
|
6
7
|
export type Availability =
|
|
@@ -28,7 +29,11 @@ export type DelegationResult =
|
|
|
28
29
|
readonly exitCode: number | null;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
|
-
export type QuotaSnapshot =
|
|
32
|
+
export type QuotaSnapshot =
|
|
33
|
+
| AgyQuotaSnapshot
|
|
34
|
+
| CodexQuotaSnapshot
|
|
35
|
+
| ClaudeQuotaSnapshot
|
|
36
|
+
| GrokQuotaSnapshot;
|
|
32
37
|
|
|
33
38
|
export interface ImageGenRequest {
|
|
34
39
|
readonly prompt: string;
|
package/src/drivers.ts
CHANGED
package/src/models.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Models are registered by canonical, provider-qualified slug —
|
|
5
5
|
* `<vendor>-<cli>/<model>[-<effort>]`, e.g. `openai-codex/gpt-5.6-sol-high`.
|
|
6
6
|
* Canonical slugs only — no short aliases, by design. That holds on both sides:
|
|
7
|
-
* `backendModel` is a pinned model id (`claude-
|
|
7
|
+
* `backendModel` is a pinned model id (`claude-sonnet-5`), never a moving vendor
|
|
8
8
|
* alias (`opus`), so a seat never silently changes model under you.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -31,7 +31,7 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
31
31
|
backend: 'grok',
|
|
32
32
|
backendModel: 'grok-4.5',
|
|
33
33
|
efforts: ['low', 'medium', 'high'],
|
|
34
|
-
brief: 'xAI Grok 4.5 via grok CLI —
|
|
34
|
+
brief: 'xAI Grok 4.5 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight',
|
|
35
35
|
},
|
|
36
36
|
'google-antigravity/gemini-3.6-flash': {
|
|
37
37
|
slug: 'google-antigravity/gemini-3.6-flash',
|
|
@@ -40,7 +40,17 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
40
40
|
efforts: ['low', 'medium', 'high'],
|
|
41
41
|
defaultEffort: 'high',
|
|
42
42
|
brief:
|
|
43
|
-
'Google Gemini 3.6 Flash via agy —
|
|
43
|
+
'Google Gemini 3.6 Flash via agy — own Antigravity login; quota shared across all Gemini tiers',
|
|
44
|
+
},
|
|
45
|
+
'google-antigravity/gemini-3.1-pro': {
|
|
46
|
+
slug: 'google-antigravity/gemini-3.1-pro',
|
|
47
|
+
backend: 'agy',
|
|
48
|
+
// agy exposes only -high and -low for this class — there is no medium tier.
|
|
49
|
+
efforts: ['low', 'high'],
|
|
50
|
+
backendModel: 'gemini-3.1-pro',
|
|
51
|
+
defaultEffort: 'high',
|
|
52
|
+
brief:
|
|
53
|
+
'Google Gemini 3.1 Pro via agy — own Antigravity login; quota shared across all Gemini tiers',
|
|
44
54
|
},
|
|
45
55
|
'google-antigravity/claude-sonnet-4-6': {
|
|
46
56
|
slug: 'google-antigravity/claude-sonnet-4-6',
|
|
@@ -68,30 +78,53 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
68
78
|
backend: 'codex',
|
|
69
79
|
backendModel: 'gpt-5.6-sol',
|
|
70
80
|
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
71
|
-
brief: 'OpenAI
|
|
81
|
+
brief: 'OpenAI gpt-5.6-sol via codex CLI — frontier agentic coding; own ChatGPT login',
|
|
72
82
|
},
|
|
73
|
-
'
|
|
74
|
-
slug: '
|
|
83
|
+
'openai-codex/gpt-5.6-terra': {
|
|
84
|
+
slug: 'openai-codex/gpt-5.6-terra',
|
|
85
|
+
backend: 'codex',
|
|
86
|
+
backendModel: 'gpt-5.6-terra',
|
|
87
|
+
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
88
|
+
brief: 'OpenAI gpt-5.6-terra via codex CLI — balanced, everyday coding; own ChatGPT login',
|
|
89
|
+
},
|
|
90
|
+
'openai-codex/gpt-5.6-luna': {
|
|
91
|
+
slug: 'openai-codex/gpt-5.6-luna',
|
|
92
|
+
backend: 'codex',
|
|
93
|
+
backendModel: 'gpt-5.6-luna',
|
|
94
|
+
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
95
|
+
brief: 'OpenAI gpt-5.6-luna via codex CLI — fast and affordable coding; own ChatGPT login',
|
|
96
|
+
},
|
|
97
|
+
'anthropic-claude/fable-5': {
|
|
98
|
+
slug: 'anthropic-claude/fable-5',
|
|
75
99
|
backend: 'claude',
|
|
76
|
-
backendModel: 'claude-
|
|
100
|
+
backendModel: 'claude-fable-5',
|
|
77
101
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
78
|
-
|
|
102
|
+
defaultEffort: 'high',
|
|
103
|
+
brief:
|
|
104
|
+
'Claude Fable 5 via claude CLI — hardest, longest-running work; bills the claude CLI subscription',
|
|
79
105
|
},
|
|
80
106
|
'anthropic-claude/opus-5': {
|
|
81
107
|
slug: 'anthropic-claude/opus-5',
|
|
82
108
|
backend: 'claude',
|
|
83
|
-
backendModel: 'claude-opus-5',
|
|
109
|
+
backendModel: 'claude-opus-5[1m]',
|
|
84
110
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
85
111
|
defaultEffort: 'high',
|
|
86
|
-
brief:
|
|
112
|
+
brief:
|
|
113
|
+
'Claude Opus 5, 1M context via claude CLI — everyday complex work; bills the claude CLI subscription',
|
|
114
|
+
},
|
|
115
|
+
'anthropic-claude/sonnet-5': {
|
|
116
|
+
slug: 'anthropic-claude/sonnet-5',
|
|
117
|
+
backend: 'claude',
|
|
118
|
+
backendModel: 'claude-sonnet-5',
|
|
119
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
120
|
+
brief: 'Claude Sonnet 5 via claude CLI — routine work; bills the claude CLI subscription',
|
|
87
121
|
},
|
|
88
|
-
'anthropic-claude/
|
|
89
|
-
slug: 'anthropic-claude/
|
|
122
|
+
'anthropic-claude/haiku-4-5': {
|
|
123
|
+
slug: 'anthropic-claude/haiku-4-5',
|
|
90
124
|
backend: 'claude',
|
|
91
|
-
backendModel: 'claude-
|
|
125
|
+
backendModel: 'claude-haiku-4-5-20251001',
|
|
92
126
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
93
|
-
|
|
94
|
-
brief: 'Claude Opus 5, 1M context via claude CLI — long-context work; bills subscription',
|
|
127
|
+
brief: 'Claude Haiku 4.5 via claude CLI — quick answers; bills the claude CLI subscription',
|
|
95
128
|
},
|
|
96
129
|
} as const satisfies Record<string, ModelSpec>;
|
|
97
130
|
|
|
@@ -1,8 +1,13 @@
|
|
|
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
|
+
} from './quotaPreflight.ts';
|
|
6
11
|
|
|
7
12
|
test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', () => {
|
|
8
13
|
const snapshot: AgyQuotaSnapshot = {
|
|
@@ -176,3 +181,43 @@ test('evaluateCodexPreflight: healthy (77% used) returns ok:true', () => {
|
|
|
176
181
|
|
|
177
182
|
assert.deepEqual(result, { ok: true });
|
|
178
183
|
});
|
|
184
|
+
|
|
185
|
+
test('evaluateGrokPreflight: usedPercent at 100 refuses with resetAt', () => {
|
|
186
|
+
const snapshot: GrokQuotaSnapshot = {
|
|
187
|
+
fetchedAt: '2024-01-01T12:00:00Z',
|
|
188
|
+
periodType: 'weekly',
|
|
189
|
+
periodStart: '2024-01-01T00:00:00Z',
|
|
190
|
+
periodEnd: '2024-01-08T00:00:00Z',
|
|
191
|
+
usedPercent: 100,
|
|
192
|
+
products: [],
|
|
193
|
+
onDemandUsedCents: undefined,
|
|
194
|
+
onDemandCapCents: undefined,
|
|
195
|
+
prepaidBalanceCents: undefined,
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const result = evaluateGrokPreflight(snapshot);
|
|
199
|
+
|
|
200
|
+
assert.deepEqual(result, {
|
|
201
|
+
ok: false,
|
|
202
|
+
message: 'grok credit quota exhausted',
|
|
203
|
+
resetAt: '2024-01-08T00:00:00Z',
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('evaluateGrokPreflight: healthy (17% used) returns ok:true', () => {
|
|
208
|
+
const snapshot: GrokQuotaSnapshot = {
|
|
209
|
+
fetchedAt: '2024-01-01T12:00:00Z',
|
|
210
|
+
periodType: 'weekly',
|
|
211
|
+
periodStart: '2024-01-01T00:00:00Z',
|
|
212
|
+
periodEnd: '2024-01-08T00:00:00Z',
|
|
213
|
+
usedPercent: 17,
|
|
214
|
+
products: [],
|
|
215
|
+
onDemandUsedCents: undefined,
|
|
216
|
+
onDemandCapCents: undefined,
|
|
217
|
+
prepaidBalanceCents: undefined,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const result = evaluateGrokPreflight(snapshot);
|
|
221
|
+
|
|
222
|
+
assert.deepEqual(result, { ok: true });
|
|
223
|
+
});
|
package/src/quotaPreflight.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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';
|
|
3
4
|
import { backendModelId, type ResolvedModel } from './models.ts';
|
|
4
5
|
|
|
5
6
|
export type PreflightVerdict =
|
|
@@ -51,11 +52,27 @@ export function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): PreflightV
|
|
|
51
52
|
return { ok: true };
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export function evaluateGrokPreflight(snapshot: GrokQuotaSnapshot): PreflightVerdict {
|
|
56
|
+
if (snapshot.usedPercent !== undefined && snapshot.usedPercent >= 100) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: 'grok credit quota exhausted',
|
|
60
|
+
resetAt: snapshot.periodEnd,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { ok: true };
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
export async function preflightModel(resolved: ResolvedModel): Promise<PreflightVerdict> {
|
|
55
68
|
if (resolved.spec.backend === 'codex') {
|
|
56
69
|
return preflightCodex();
|
|
57
70
|
}
|
|
58
71
|
|
|
72
|
+
if (resolved.spec.backend === 'grok') {
|
|
73
|
+
return preflightGrok();
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
if (resolved.spec.backend !== 'agy') {
|
|
60
77
|
return { ok: true };
|
|
61
78
|
}
|
|
@@ -83,6 +100,18 @@ export async function preflightCodex(): Promise<PreflightVerdict> {
|
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
|
|
103
|
+
export async function preflightGrok(): Promise<PreflightVerdict> {
|
|
104
|
+
try {
|
|
105
|
+
const snapshot = await fetchGrokQuota();
|
|
106
|
+
return evaluateGrokPreflight(snapshot);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
warning: `quota preflight failed (${(err as Error).message}); proceeding`,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
function formatReset(resetTime: string | undefined): string {
|
|
87
116
|
if (!resetTime) return '-';
|
|
88
117
|
const ms = new Date(resetTime).getTime() - Date.now();
|