@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.
@@ -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
- agy.status === 'rejected' && codex.status === 'rejected' && claude.status === 'rejected';
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 (off-budget). The claude-backend slugs are FALLBACKS for',
12
- 'when the off-budget CLIs are quota-exhaustedthey bill your Claude subscription.',
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({
@@ -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.5');
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 = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot;
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
@@ -15,6 +15,7 @@ const agyDriver: AgentCliDriver = {
15
15
  const grokDriver: AgentCliDriver = {
16
16
  probe: () => grok.probe(),
17
17
  run: task => grok.run(task),
18
+ quota: () => grok.fetchGrokQuota(),
18
19
  generateImage: req => grok.generateImage(req),
19
20
  };
20
21
 
@@ -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.5', '--out', 'plan.md', 'do something']);
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.5',
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.5',
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.5',
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.5', 'hello agent']);
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.5',
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.5',
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.5',
116
+ model: 'xai-grok/grok-4.6',
117
117
  tools: false,
118
118
  preflight: false,
119
119
  json: false,
@@ -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.5');
13
+ const grok = resolveModel('xai-grok/grok-4.6');
14
14
  expect(grok).toBeDefined();
15
- expect(grok?.spec.slug).toBe('xai-grok/grok-4.5');
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.6-flash');
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.6-flash');
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.5-medium');
42
- expect(grokMedium?.spec.slug).toBe('xai-grok/grok-4.5');
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.5-xhigh');
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.6-flash');
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.6-flash-high');
74
+ expect(backendModelId(gemini)).toBe('gemini-3.7-flash-high');
75
75
 
76
- const geminiLow = resolveModel('google-antigravity/gemini-3.6-flash-low');
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.6-flash-low');
78
+ expect(backendModelId(geminiLow)).toBe('gemini-3.7-flash-low');
79
79
 
80
- const grok = resolveModel('xai-grok/grok-4.5');
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.5');
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.6-flash seats as image-gen capable', () => {
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.5');
92
- const gemini = resolveModel('google-antigravity/gemini-3.6-flash');
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.5');
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.6-flash');
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.5');
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.6-flash');
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-opus-5`), never a moving vendor
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 — default for plan & review; own xAI login',
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 — default for implement, also image-gen; own Antigravity login',
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 Codex gpt-5.6-sol via codex CLI',
101
+ brief: 'OpenAI gpt-5.6-sol via codex CLI — frontier agentic coding; own ChatGPT login',
72
102
  },
73
- 'anthropic-claude/sonnet-5': {
74
- slug: 'anthropic-claude/sonnet-5',
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-sonnet-5',
120
+ backendModel: 'claude-fable-5',
77
121
  efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
78
- brief: 'Claude Sonnet 5 via claude CLI — bills your Claude subscription',
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: 'Claude Opus 5 via claude CLI (default effort: high) — bills subscription',
132
+ brief:
133
+ 'Claude Opus 5, 1M context via claude CLI — everyday complex work; bills the claude CLI subscription',
87
134
  },
88
- 'anthropic-claude/opus-5-1m': {
89
- slug: 'anthropic-claude/opus-5-1m',
135
+ 'anthropic-claude/sonnet-5': {
136
+ slug: 'anthropic-claude/sonnet-5',
90
137
  backend: 'claude',
91
- backendModel: 'claude-opus-5[1m]',
138
+ backendModel: 'claude-sonnet-5',
92
139
  efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
93
- defaultEffort: 'high',
94
- brief: 'Claude Opus 5, 1M context via claude CLI — long-context work; bills subscription',
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