@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
|
@@ -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.
|
|
12
|
-
'
|
|
11
|
+
'Recommended first choice: xai-grok/grok-4.6. 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/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/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/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
|
@@ -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
|
|
|
@@ -26,13 +26,33 @@ 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',
|
|
32
41
|
backendModel: 'grok-4.5',
|
|
33
42
|
efforts: ['low', 'medium', 'high'],
|
|
34
|
-
brief: 'xAI Grok 4.5 via grok CLI —
|
|
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',
|
|
@@ -40,7 +60,17 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
40
60
|
efforts: ['low', 'medium', 'high'],
|
|
41
61
|
defaultEffort: 'high',
|
|
42
62
|
brief:
|
|
43
|
-
'Google Gemini 3.6 Flash via agy —
|
|
63
|
+
'Google Gemini 3.6 Flash via agy — own Antigravity login; quota shared across all Gemini tiers',
|
|
64
|
+
},
|
|
65
|
+
'google-antigravity/gemini-3.1-pro': {
|
|
66
|
+
slug: 'google-antigravity/gemini-3.1-pro',
|
|
67
|
+
backend: 'agy',
|
|
68
|
+
// agy exposes only -high and -low for this class — there is no medium tier.
|
|
69
|
+
efforts: ['low', 'high'],
|
|
70
|
+
backendModel: 'gemini-3.1-pro',
|
|
71
|
+
defaultEffort: 'high',
|
|
72
|
+
brief:
|
|
73
|
+
'Google Gemini 3.1 Pro via agy — own Antigravity login; quota shared across all Gemini tiers',
|
|
44
74
|
},
|
|
45
75
|
'google-antigravity/claude-sonnet-4-6': {
|
|
46
76
|
slug: 'google-antigravity/claude-sonnet-4-6',
|
|
@@ -68,30 +98,53 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
68
98
|
backend: 'codex',
|
|
69
99
|
backendModel: 'gpt-5.6-sol',
|
|
70
100
|
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
71
|
-
brief: 'OpenAI
|
|
101
|
+
brief: 'OpenAI gpt-5.6-sol via codex CLI — frontier agentic coding; own ChatGPT login',
|
|
72
102
|
},
|
|
73
|
-
'
|
|
74
|
-
slug: '
|
|
103
|
+
'openai-codex/gpt-5.6-terra': {
|
|
104
|
+
slug: 'openai-codex/gpt-5.6-terra',
|
|
105
|
+
backend: 'codex',
|
|
106
|
+
backendModel: 'gpt-5.6-terra',
|
|
107
|
+
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
108
|
+
brief: 'OpenAI gpt-5.6-terra via codex CLI — balanced, everyday coding; own ChatGPT login',
|
|
109
|
+
},
|
|
110
|
+
'openai-codex/gpt-5.6-luna': {
|
|
111
|
+
slug: 'openai-codex/gpt-5.6-luna',
|
|
112
|
+
backend: 'codex',
|
|
113
|
+
backendModel: 'gpt-5.6-luna',
|
|
114
|
+
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
115
|
+
brief: 'OpenAI gpt-5.6-luna via codex CLI — fast and affordable coding; own ChatGPT login',
|
|
116
|
+
},
|
|
117
|
+
'anthropic-claude/fable-5': {
|
|
118
|
+
slug: 'anthropic-claude/fable-5',
|
|
75
119
|
backend: 'claude',
|
|
76
|
-
backendModel: 'claude-
|
|
120
|
+
backendModel: 'claude-fable-5',
|
|
77
121
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
78
|
-
|
|
122
|
+
defaultEffort: 'high',
|
|
123
|
+
brief:
|
|
124
|
+
'Claude Fable 5 via claude CLI — hardest, longest-running work; bills the claude CLI subscription',
|
|
79
125
|
},
|
|
80
126
|
'anthropic-claude/opus-5': {
|
|
81
127
|
slug: 'anthropic-claude/opus-5',
|
|
82
128
|
backend: 'claude',
|
|
83
|
-
backendModel: 'claude-opus-5',
|
|
129
|
+
backendModel: 'claude-opus-5[1m]',
|
|
84
130
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
85
131
|
defaultEffort: 'high',
|
|
86
|
-
brief:
|
|
132
|
+
brief:
|
|
133
|
+
'Claude Opus 5, 1M context via claude CLI — everyday complex work; bills the claude CLI subscription',
|
|
87
134
|
},
|
|
88
|
-
'anthropic-claude/
|
|
89
|
-
slug: 'anthropic-claude/
|
|
135
|
+
'anthropic-claude/sonnet-5': {
|
|
136
|
+
slug: 'anthropic-claude/sonnet-5',
|
|
90
137
|
backend: 'claude',
|
|
91
|
-
backendModel: 'claude-
|
|
138
|
+
backendModel: 'claude-sonnet-5',
|
|
92
139
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
brief: 'Claude Sonnet 5 via claude CLI — routine work; bills the claude CLI subscription',
|
|
141
|
+
},
|
|
142
|
+
'anthropic-claude/haiku-4-5': {
|
|
143
|
+
slug: 'anthropic-claude/haiku-4-5',
|
|
144
|
+
backend: 'claude',
|
|
145
|
+
backendModel: 'claude-haiku-4-5-20251001',
|
|
146
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
147
|
+
brief: 'Claude Haiku 4.5 via claude CLI — quick answers; bills the claude CLI subscription',
|
|
95
148
|
},
|
|
96
149
|
} as const satisfies Record<string, ModelSpec>;
|
|
97
150
|
|