jeo-code 0.7.56 → 0.7.58

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.
@@ -130,7 +130,14 @@ export interface Config {
130
130
  * Model role tiers (gjc `--smol`/`--slow`/`--plan` parity). Each falls back to
131
131
  * `defaultModel`. Env `JEO_SMOL_MODEL`/`JEO_SLOW_MODEL`/`JEO_PLAN_MODEL` fill gaps.
132
132
  */
133
- roles?: { smol?: string; slow?: string; plan?: string };
133
+ roles?: {
134
+ smol?: string;
135
+ medium?: string;
136
+ high?: string;
137
+ xhigh?: string;
138
+ slow?: string;
139
+ plan?: string;
140
+ };
134
141
  /** Prompt-content-based per-turn model routing (PromptRouter) — see prompt-router.ts.
135
142
  * Opt-in (undefined/false = off). Never affects subagent/role-tier model resolution. */
136
143
  routing?: {
@@ -139,8 +146,19 @@ export interface Config {
139
146
  tiers?: {
140
147
  trivial?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
141
148
  standard?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
149
+ /** A single borderline-complex signal (see prompt-router.ts's `classifyPromptHeuristically`)
150
+ * — routes to `roles.high` by default instead of jumping straight to `complex`/xhigh. */
151
+ high?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
142
152
  complex?: { model?: string; thinking?: "low" | "medium" | "high" | "xhigh" };
143
153
  };
154
+ /** Opt-in (default off): for `standard`/`high` tiers left unconfigured (no
155
+ * `routing.tiers.*.model`, no `roles.medium`/`roles.high`), pick a SESSION-STABLE
156
+ * model from the cross-provider equivalence pool (`tierModelPool`/`selectFromPool`)
157
+ * instead of falling straight to `defaultModel` — spreads different sessions across
158
+ * every credentialed provider's comparable-tier model so jeo actually captures each
159
+ * provider's respective strengths on comparably-priced work, rather than every
160
+ * session converging on one provider. */
161
+ crossProviderPool?: boolean;
144
162
  };
145
163
  gitAutoCommit?: boolean;
146
164
  hooks?: HookConfig;
@@ -284,6 +302,9 @@ function withEnvOverlay(cfg: Config): Config {
284
302
  lmstudioBaseUrl: cfg.lmstudioBaseUrl || process.env.LMSTUDIO_BASE_URL || "http://localhost:1234/v1",
285
303
  roles: {
286
304
  smol: cfg.roles?.smol || jeoEnv("SMOL_MODEL"),
305
+ medium: cfg.roles?.medium || jeoEnv("MEDIUM_MODEL"),
306
+ high: cfg.roles?.high || jeoEnv("HIGH_MODEL"),
307
+ xhigh: cfg.roles?.xhigh || jeoEnv("XHIGH_MODEL"),
287
308
  slow: cfg.roles?.slow || jeoEnv("SLOW_MODEL"),
288
309
  plan: cfg.roles?.plan || jeoEnv("PLAN_MODEL"),
289
310
  },
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { DEFAULT_TOOLS, TOOL_PROTOCOL, READONLY_TOOL_PROTOCOL, WORKING_DISCIPLINE, type ToolHandler } from "./engine";
13
13
  import type { Config } from "./state";
14
+ import type { CatalogModel } from "../ai/model-catalog";
14
15
  import architectPrompt from "../prompts/agents/architect.md" with { type: "text" };
15
16
  import criticPrompt from "../prompts/agents/critic.md" with { type: "text" };
16
17
  import executorPrompt from "../prompts/agents/executor.md" with { type: "text" };
@@ -158,9 +159,42 @@ export function defaultSubagentRole(): SubagentRole {
158
159
  export type SubagentConfig = NonNullable<Config["subagents"]>;
159
160
 
160
161
  /** Per-role model override → falls back to the global default model. */
161
- export function resolveSubagentModel(roleId: string, config: Pick<Config, "defaultModel" | "subagents">): string {
162
- const entry = config.subagents?.[normalizeRoleId(roleId)];
163
- return entry?.model || config.defaultModel;
162
+ export function resolveSubagentModel(
163
+ roleId: string,
164
+ config: {
165
+ defaultModel: string;
166
+ subagents?: Config["subagents"];
167
+ roles?: Config["roles"];
168
+ providers?: Config["providers"];
169
+ oauth?: Config["oauth"];
170
+ },
171
+ ): string {
172
+ const normalized = normalizeRoleId(roleId);
173
+ const entry = config.subagents?.[normalized];
174
+ if (entry?.model) return entry.model;
175
+
176
+ // Exception: load strongestCredentialed dynamically using require to prevent eager loading and circular dependency issues that break test mocking.
177
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
178
+ const { strongestCredentialed } = require("./prompt-router");
179
+ if (normalized === "executor") {
180
+ return config.roles?.xhigh || config.roles?.slow || strongestCredentialed(config) || config.defaultModel;
181
+ }
182
+ if (normalized === "architect") {
183
+ return config.roles?.xhigh || strongestCredentialed(config, (m: CatalogModel) => m.thinking.includes("xhigh") || m.thinking.includes("high")) || config.defaultModel;
184
+ }
185
+ if (normalized === "planner") {
186
+ return config.roles?.high || strongestCredentialed(config) || config.defaultModel;
187
+ }
188
+ if (normalized === "critic") {
189
+ return (
190
+ config.roles?.medium ||
191
+ config.roles?.smol ||
192
+ strongestCredentialed(config, (m: CatalogModel) => m.canonical === "o3-mini") ||
193
+ strongestCredentialed(config, (m: CatalogModel) => m.canonical === "gemini-2.5-flash") ||
194
+ config.defaultModel
195
+ );
196
+ }
197
+ return config.defaultModel;
164
198
  }
165
199
 
166
200
  /** Per-role step budget → config override, else the role default, else 15. */
@@ -1130,3 +1130,172 @@ export async function deleteTool(
1130
1130
  return { success: false, output: "", error: err.message };
1131
1131
  }
1132
1132
  }
1133
+
1134
+
1135
+ /**
1136
+ * Binary-search git history to find the commit that introduced a regression.
1137
+ * Uses `git bisect run <predicate>` to automate the search.
1138
+ * Exit codes: 0=good, non-zero=bad, 125=skip.
1139
+ */
1140
+ export async function bisectTool(
1141
+ good: string,
1142
+ bad: string = "HEAD",
1143
+ run: string = "",
1144
+ invert: boolean = false,
1145
+ maxSteps: number = 40,
1146
+ stepTimeoutMs: number = 600000,
1147
+ cwd: string = process.cwd()
1148
+ ): Promise<ToolResult> {
1149
+ try {
1150
+ if (!good || !good.trim()) {
1151
+ return { success: false, output: "", error: 'bisect requires "good" (a known-good commit)' };
1152
+ }
1153
+ if (!run || !run.trim()) {
1154
+ return { success: false, output: "", error: 'bisect requires "run" (a shell command to test)' };
1155
+ }
1156
+ if (maxSteps <= 0) {
1157
+ return { success: false, output: "", error: 'maxSteps must be > 0' };
1158
+ }
1159
+ if (stepTimeoutMs <= 0) {
1160
+ return { success: false, output: "", error: 'stepTimeoutMs must be > 0' };
1161
+ }
1162
+
1163
+ // Helper to run a command with timeout support and resilient error handling
1164
+ const runCmd = async (args: string[], timeoutMs?: number): Promise<{ code: number; out: string; err: string }> => {
1165
+ const timeout = timeoutMs || stepTimeoutMs;
1166
+ let timeoutOccurred = false;
1167
+ const timeoutHandle = setTimeout(() => {
1168
+ timeoutOccurred = true;
1169
+ }, timeout);
1170
+
1171
+ try {
1172
+ const proc = Bun.spawn(args, {
1173
+ cwd,
1174
+ stdout: "pipe",
1175
+ stderr: "pipe",
1176
+ });
1177
+
1178
+ let out = "";
1179
+ let err = "";
1180
+
1181
+ const stdoutReader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
1182
+ const stderrReader = (proc.stderr as ReadableStream<Uint8Array>).getReader();
1183
+
1184
+ // Read stdout with resilience: stream errors don't fail the entire call.
1185
+ // If a reader encounters an error, we continue with partial output, as
1186
+ // git bisect can still function with partial stdout/stderr.
1187
+ try {
1188
+ while (!timeoutOccurred) {
1189
+ const { done, value } = await stdoutReader.read();
1190
+ if (done) break;
1191
+ out += new TextDecoder().decode(value);
1192
+ }
1193
+ } catch (e) {
1194
+ // Stream read error; continue with partial output.
1195
+ // This is acceptable because git bisect may still be usable.
1196
+ }
1197
+
1198
+ // Read stderr with resilience: same strategy as stdout.
1199
+ try {
1200
+ while (!timeoutOccurred) {
1201
+ const { done, value } = await stderrReader.read();
1202
+ if (done) break;
1203
+ err += new TextDecoder().decode(value);
1204
+ }
1205
+ } catch (e) {
1206
+ // Stream read error; continue with partial output.
1207
+ }
1208
+
1209
+ if (timeoutOccurred) {
1210
+ return { code: 124, out, err: `Command timeout after ${timeout}ms` };
1211
+ }
1212
+
1213
+ const code = await proc.exited;
1214
+ return { code: typeof code === 'number' ? code : 0, out, err };
1215
+ } finally {
1216
+ clearTimeout(timeoutHandle);
1217
+ }
1218
+ };
1219
+
1220
+ // Validate commits exist (quick validation with shorter timeout)
1221
+ const goodRes = await runCmd(["git", "rev-parse", good], 10000);
1222
+ const badRes = await runCmd(["git", "rev-parse", bad], 10000);
1223
+
1224
+ if (goodRes.code !== 0) {
1225
+ return { success: false, output: "", error: `Invalid or non-existent commit: ${good}` };
1226
+ }
1227
+ if (badRes.code !== 0) {
1228
+ return { success: false, output: "", error: `Invalid or non-existent commit: ${bad}` };
1229
+ }
1230
+
1231
+ const goodSha = goodRes.out.trim();
1232
+ const badSha = badRes.out.trim();
1233
+
1234
+ // Start bisect session
1235
+ const initRes = await runCmd(["sh", "-c", `git bisect start "${badSha}" "${goodSha}"`], 10000);
1236
+ if (initRes.code !== 0) {
1237
+ return { success: false, output: "", error: `Failed to start bisect: ${initRes.err || initRes.out}` };
1238
+ }
1239
+
1240
+ // Run bisect with the predicate command.
1241
+ // Shell escaping: single quotes in sh -c context need special care.
1242
+ // 'text' -> '\''text'\'' (end quote, escaped quote, start quote, content, end quote, escaped quote, start quote)
1243
+ const escapedRun = run.replace(/'/g, "'\''");
1244
+ const runCmd2 = invert
1245
+ ? `git bisect run sh -c '${escapedRun} && exit 1 || exit 0'`
1246
+ : `git bisect run sh -c '${escapedRun}'`;
1247
+
1248
+ const runRes = await runCmd(["sh", "-c", runCmd2]);
1249
+
1250
+ // Parse result: look for "XXXX is the first bad/good commit"
1251
+ const badCommitMatch = runRes.out.match(/^([0-9a-f]{7,40}) is the first (bad|good) commit$/m);
1252
+ let culprit: string | null = null;
1253
+ let concluded = false;
1254
+
1255
+ if (badCommitMatch) {
1256
+ culprit = badCommitMatch[1];
1257
+ concluded = true;
1258
+ } else if (runRes.out.includes("only 'skip'ped commits left")) {
1259
+ concluded = false;
1260
+ }
1261
+
1262
+ // Cleanup: always attempt to reset bisect and working tree (best-effort).
1263
+ // Errors here are non-critical; if reset fails, the user can manually run
1264
+ // 'git bisect reset' to recover. We don't throw on cleanup errors.
1265
+ try {
1266
+ await runCmd(["git", "bisect", "reset"], 10000);
1267
+ } catch {}
1268
+ try {
1269
+ await runCmd(["git", "reset", "--hard"], 10000);
1270
+ } catch {}
1271
+
1272
+ if (!concluded) {
1273
+ return {
1274
+ success: false,
1275
+ output: `Bisect did not converge. Output:\n${runRes.out}`,
1276
+ error: "",
1277
+ };
1278
+ }
1279
+
1280
+ if (culprit) {
1281
+ // Get commit details (use shorter timeout for metadata lookup)
1282
+ const detailsRes = await runCmd(["git", "show", "-s", "--format=%aN|%ai|%s", culprit], 10000);
1283
+ const [author, date, subject] = detailsRes.out.trim().split("|");
1284
+ const message = [
1285
+ invert ? `First fixing commit: ${culprit}` : `First bad commit: ${culprit}`,
1286
+ author ? `Author: ${author}` : "",
1287
+ date ? `Date: ${date}` : "",
1288
+ subject ? `Subject: ${subject}` : "",
1289
+ ]
1290
+ .filter(Boolean)
1291
+ .join("\n");
1292
+
1293
+ return { success: true, output: message };
1294
+ }
1295
+
1296
+ return { success: false, output: "Bisect completed but found no culprit.", error: "" };
1297
+ } catch (err: any) {
1298
+ return { success: false, output: "", error: err.message || String(err) };
1299
+ }
1300
+ }
1301
+
@@ -19,10 +19,18 @@ export interface ModelCatalogEntry {
19
19
  recommended?: boolean;
20
20
  note?: string;
21
21
  company?: string;
22
+ /** Public release date, `"YYYY-MM"` — see `CatalogModel.releaseDate`. Drives
23
+ * `catalogForProvider`'s newest-first secondary sort. */
24
+ releaseDate?: string;
22
25
  }
23
26
 
24
- /** Canonical ids that should be surfaced as a provider's recommended default. */
25
- const RECOMMENDED = new Set(["claude-sonnet-4-6", "gpt-4o", "gemini-2.0-flash", "antigravity/gemini-3-pro-low", "qwen2.5"]);
27
+ /** Canonical ids surfaced as a provider's recommended default — the current
28
+ * (2026-07 deep-research verified) general-availability flagship per provider,
29
+ * NOT necessarily the single most expensive/most-capable model (e.g. Anthropic's
30
+ * actual top tier, claude-fable-5, is pricier than the "recommended" sonnet-5 —
31
+ * recommended means "solid everyday default", not "strongest"). Re-verify and
32
+ * update whenever a provider ships a new default-tier release. */
33
+ const RECOMMENDED: Record<string, true> = { "claude-sonnet-5": true, "gpt-5.5": true, "gemini-3-flash": true, "antigravity/gpt-5.5": true, "qwen2.5": true };
26
34
 
27
35
  export function normalizeModelId(id: string): string {
28
36
  return (id ?? "").trim().toLowerCase();
@@ -35,9 +43,10 @@ function adapt(m: CatalogModel): ModelCatalogEntry {
35
43
  provider: m.provider,
36
44
  contextWindow: m.contextTokens,
37
45
  reasoning,
38
- recommended: RECOMMENDED.has(m.canonical),
46
+ recommended: !!RECOMMENDED[m.canonical],
39
47
  note: `${formatTokens(m.contextTokens)} ctx${reasoning ? ", reasoning" : ""}`,
40
48
  company: m.company,
49
+ releaseDate: m.releaseDate,
41
50
  };
42
51
  }
43
52
 
@@ -50,12 +59,19 @@ export function findCatalogEntry(id: string): ModelCatalogEntry | undefined {
50
59
  return hit ? adapt(hit) : undefined;
51
60
  }
52
61
 
53
- /** Entries for a provider, recommended first then by id. */
62
+ /** Entries for a provider, recommended first, then NEWEST-FIRST by `releaseDate`
63
+ * (undated entries sort last, tiebroken by id) — this is the actual list order
64
+ * `/model`'s live picker and `jeo setup`'s recommendation prompt show a user. */
54
65
  export function catalogForProvider(provider: ProviderName): ModelCatalogEntry[] {
55
66
  return catalogByProvider(provider)
56
67
  .map(adapt)
57
68
  .sort((a, b) => {
58
69
  if (!!b.recommended !== !!a.recommended) return b.recommended ? 1 : -1;
70
+ if (a.releaseDate !== b.releaseDate) {
71
+ if (!a.releaseDate) return 1;
72
+ if (!b.releaseDate) return -1;
73
+ return b.releaseDate.localeCompare(a.releaseDate);
74
+ }
59
75
  return a.id.localeCompare(b.id);
60
76
  });
61
77
  }
@@ -29,35 +29,66 @@ export interface CatalogModel {
29
29
  images: boolean;
30
30
  /** Optional company override. */
31
31
  company?: string;
32
+ /** True for models requiring separate approval/waitlist beyond the provider's
33
+ * base credential (e.g. Anthropic's invite-only Project Glasswing models) — a
34
+ * user with a valid API key/OAuth for the PROVIDER may still lack access to
35
+ * this SPECIFIC model. Auto-select (prompt-router.ts's pool/single-winner
36
+ * selection) excludes these; explicit `/model` or `routing.tiers.*.model`
37
+ * can still target them by id for approved accounts. */
38
+ limitedAvailability?: boolean;
39
+ /** Public release date, `"YYYY-MM"` (lexicographically sortable) — deep-research
40
+ * sourced from each provider's own announcement. Drives newest-first catalog
41
+ * ordering and recency-based tiebreaks in prompt-router.ts's ranking functions
42
+ * (`compareStrengthAscending`, `cheapestCredentialed`, `strongestCredentialed`)
43
+ * so a genuinely newer same-tier model wins a tie instead of an arbitrary
44
+ * alphabetical pick. Omitted (undefined) for ids with no confirmed public
45
+ * date — these sort AFTER every dated entry within their block, never before,
46
+ * so an unconfirmed date can never silently outrank a verified-newer model. */
47
+ releaseDate?: string;
32
48
  }
33
49
 
34
50
  const FULL: ThinkLevel[] = ["low", "medium", "high", "xhigh"];
35
51
  const STD: ThinkLevel[] = ["low", "medium", "high"];
36
52
 
53
+ // [canonical model id, release YYYY-MM] tuples, deep-research sourced (Anthropic/
54
+ // OpenAI/Google base-model announcement dates — antigravity is a hosting layer over
55
+ // the SAME underlying models, not a separate release). Newest-first per family below;
56
+ // -thinking/-high/-low suffixes are reasoning-effort variants of the SAME release.
37
57
  export const ANTIGRAVITY_MODELS = [
38
- "claude-opus-4-6-thinking",
39
- "claude-opus-4-7",
40
- "claude-opus-4-7-thinking",
41
- "claude-opus-4-8",
42
- "claude-opus-4-8-thinking",
43
- "claude-sonnet-4-6",
44
- "claude-sonnet-4-6-thinking",
45
- "gemini-2.5-flash",
46
- "gemini-2.5-flash-thinking",
47
- "gemini-2.5-pro",
48
- "gemini-3-flash",
49
- "gemini-3-pro-high",
50
- "gemini-3-pro-low",
51
- "gemini-3.1-pro-high",
52
- "gemini-3.1-pro-low",
53
- "gpt-oss-120b-medium",
54
- "gpt-5.5",
58
+ ["gpt-5.5", "2026-04"],
59
+ ["gemini-3.1-pro-high", "2026-02"],
60
+ ["gemini-3.1-pro-low", "2026-02"],
61
+ ["claude-opus-4-8", "2026-05"],
62
+ ["claude-opus-4-8-thinking", "2026-05"],
63
+ ["claude-opus-4-7", "2026-04"],
64
+ ["claude-opus-4-7-thinking", "2026-04"],
65
+ ["claude-sonnet-4-6", "2026-02"],
66
+ ["claude-sonnet-4-6-thinking", "2026-02"],
67
+ ["claude-opus-4-6-thinking", "2026-02"],
68
+ ["gemini-3-flash", "2025-12"],
69
+ ["gemini-3-pro-high", "2025-11"],
70
+ ["gemini-3-pro-low", "2025-11"],
71
+ ["gpt-oss-120b-medium", "2025-08"],
72
+ ["gemini-2.5-flash", "2025-06"],
73
+ ["gemini-2.5-flash-thinking", "2025-06"],
74
+ ["gemini-2.5-pro", "2025-03"],
55
75
  ] as const;
56
76
 
57
- /** A curated set of common public models with their documented capabilities. */
77
+ /** A curated set of common public models with their documented capabilities.
78
+ * Ordered NEWEST-FIRST within each provider block per `releaseDate`
79
+ * (deep-research sourced from each provider's own announcement — see the
80
+ * v0.7.58 CHANGELOG entry for the full per-model source list). Purely for
81
+ * human readability / "what's current" at a glance — routing correctness
82
+ * does not depend on array order: `prompt-router.ts`'s ranking functions
83
+ * (`cheapestCredentialed`/`strongestCredentialed`/`compareStrengthAscending`)
84
+ * now tiebreak on `releaseDate` directly rather than relying on position. */
58
85
  export const MODEL_CATALOG: readonly CatalogModel[] = [
59
- // Anthropic
60
- { canonical: "claude-haiku-4-5", provider: "anthropic", providerModel: "claude-haiku-4-5-20251001", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
86
+ // Anthropic (newest first: sonnet-5 > fable-5 > opus-4-8 > opus-4-7 > sonnet-4-6 > opus-4-6 > haiku-4-5)
87
+ { canonical: "claude-sonnet-5", provider: "anthropic", providerModel: "claude-sonnet-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
88
+ // Fable 5 = Anthropic's most capable widely-released model (adaptive thinking always on);
89
+ // Mythos 5 is limited-availability (Project Glasswing) but callable by id for approved accounts.
90
+ { canonical: "claude-fable-5", provider: "anthropic", providerModel: "claude-fable-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-06" },
91
+ { canonical: "claude-mythos-5", provider: "anthropic", providerModel: "claude-mythos-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, limitedAvailability: true, releaseDate: "2026-06" },
61
92
  // NOTE: opus 4.6+ use Anthropic ADAPTIVE thinking (type:"adaptive" + output_config.effort).
62
93
  // opus 4.7/4.8 OMIT visible thought unless the request opts into `display: "summarized"` —
63
94
  // anthropic.ts sets that on the adaptive transport so reasoning streams again (gjc parity).
@@ -65,50 +96,54 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
65
96
  // The 4.6 generation onward is 1M context / 128k sync max output (Anthropic docs:
66
97
  // Opus 4.8 & Sonnet 5 comparison table; Opus 4.6/4.7, Sonnet 4.6 share the gen's
67
98
  // dateless-snapshot 1M/128k + 300k batch-output beta). Dated pre-4.6 ids keep 200k/64k.
68
- { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
69
- { canonical: "claude-opus-4-7", provider: "anthropic", providerModel: "claude-opus-4-7", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
70
- { canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
71
- { canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
72
- { canonical: "claude-sonnet-5", provider: "anthropic", providerModel: "claude-sonnet-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
73
- // Fable 5 = Anthropic's most capable widely-released model (adaptive thinking always on);
74
- // Mythos 5 is limited-availability (Project Glasswing) but callable by id for approved accounts.
75
- { canonical: "claude-fable-5", provider: "anthropic", providerModel: "claude-fable-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
76
- { canonical: "claude-mythos-5", provider: "anthropic", providerModel: "claude-mythos-5", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
77
- // OpenAI
78
- { canonical: "gpt-4o", provider: "openai", providerModel: "gpt-4o", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
79
- { canonical: "gpt-4o-mini", provider: "openai", providerModel: "gpt-4o-mini", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
80
- { canonical: "gpt-4.1", provider: "openai", providerModel: "gpt-4.1", contextTokens: 1_000_000, maxOutputTokens: 32_768, thinking: [], images: true },
81
- { canonical: "o3", provider: "openai", providerModel: "o3", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true },
82
- { canonical: "o3-mini", provider: "openai", providerModel: "o3-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: false },
83
- { canonical: "o4-mini", provider: "openai", providerModel: "o4-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true },
84
- { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
85
- { canonical: "gpt-5.4", provider: "openai", providerModel: "gpt-5.4", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true },
86
- // xAI (Grok) — OpenAI-compatible at https://api.x.ai/v1 (XAI_API_KEY)
87
- { canonical: "grok-4.3", provider: "xai", providerModel: "grok-4.3", contextTokens: 256_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
88
- { canonical: "grok-4-fast-reasoning", provider: "xai", providerModel: "grok-4-fast-reasoning", contextTokens: 2_000_000, maxOutputTokens: 64_000, thinking: FULL, images: true },
89
- { canonical: "grok-4-fast-non-reasoning", provider: "xai", providerModel: "grok-4-fast-non-reasoning", contextTokens: 2_000_000, maxOutputTokens: 64_000, thinking: [], images: true },
90
- { canonical: "grok-code-fast-1", provider: "xai", providerModel: "grok-code-fast-1", contextTokens: 256_000, maxOutputTokens: 64_000, thinking: FULL, images: false },
91
- // Kimi (Moonshot) — OpenAI-compatible at https://api.moonshot.ai/v1 (KIMI_API_KEY)
92
- { canonical: "kimi-k2-0711-preview", provider: "kimi", providerModel: "kimi-k2-0711-preview", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false },
93
- { canonical: "kimi-thinking-preview", provider: "kimi", providerModel: "kimi-thinking-preview", contextTokens: 128_000, maxOutputTokens: 32_000, thinking: FULL, images: true },
94
- { canonical: "kimi-latest", provider: "kimi", providerModel: "kimi-latest", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true },
95
- { canonical: "moonshot-v1-128k", provider: "kimi", providerModel: "moonshot-v1-128k", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false },
99
+ { canonical: "claude-opus-4-8", provider: "anthropic", providerModel: "claude-opus-4-8", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-05" },
100
+ { canonical: "claude-opus-4-7", provider: "anthropic", providerModel: "claude-opus-4-7", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
101
+ { canonical: "claude-sonnet-4-6", provider: "anthropic", providerModel: "claude-sonnet-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
102
+ { canonical: "claude-opus-4-6", provider: "anthropic", providerModel: "claude-opus-4-6", contextTokens: 1_000_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-02" },
103
+ { canonical: "claude-haiku-4-5", provider: "anthropic", providerModel: "claude-haiku-4-5-20251001", contextTokens: 200_000, maxOutputTokens: 64_000, thinking: FULL, images: true, releaseDate: "2025-10" },
104
+ // OpenAI (newest first: gpt-5.5 > gpt-5.4 > o4-mini/o3/gpt-4.1 > o3-mini > gpt-4o-mini > gpt-4o)
105
+ { canonical: "gpt-5.5", provider: "openai", providerModel: "gpt-5.5", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-04" },
106
+ { canonical: "gpt-5.4", provider: "openai", providerModel: "gpt-5.4", contextTokens: 400_000, maxOutputTokens: 128_000, thinking: FULL, images: true, releaseDate: "2026-03" },
107
+ { canonical: "o4-mini", provider: "openai", providerModel: "o4-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true, releaseDate: "2025-04" },
108
+ { canonical: "o3", provider: "openai", providerModel: "o3", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: true, releaseDate: "2025-04" },
109
+ { canonical: "gpt-4.1", provider: "openai", providerModel: "gpt-4.1", contextTokens: 1_000_000, maxOutputTokens: 32_768, thinking: [], images: true, releaseDate: "2025-04" },
110
+ { canonical: "o3-mini", provider: "openai", providerModel: "o3-mini", contextTokens: 200_000, maxOutputTokens: 100_000, thinking: STD, images: false, releaseDate: "2025-01" },
111
+ { canonical: "gpt-4o-mini", provider: "openai", providerModel: "gpt-4o-mini", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true, releaseDate: "2024-07" },
112
+ { canonical: "gpt-4o", provider: "openai", providerModel: "gpt-4o", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true, releaseDate: "2024-05" },
113
+ // xAI (Grok) — OpenAI-compatible at https://api.x.ai/v1 (XAI_API_KEY). Newest first.
114
+ { canonical: "grok-4.3", provider: "xai", providerModel: "grok-4.3", contextTokens: 256_000, maxOutputTokens: 64_000, thinking: FULL, images: true, releaseDate: "2026-04" },
115
+ { canonical: "grok-4-fast-reasoning", provider: "xai", providerModel: "grok-4-fast-reasoning", contextTokens: 2_000_000, maxOutputTokens: 64_000, thinking: FULL, images: true, releaseDate: "2025-09" },
116
+ { canonical: "grok-4-fast-non-reasoning", provider: "xai", providerModel: "grok-4-fast-non-reasoning", contextTokens: 2_000_000, maxOutputTokens: 64_000, thinking: [], images: true, releaseDate: "2025-09" },
117
+ { canonical: "grok-code-fast-1", provider: "xai", providerModel: "grok-code-fast-1", contextTokens: 256_000, maxOutputTokens: 64_000, thinking: FULL, images: false, releaseDate: "2025-08" },
118
+ // Kimi (Moonshot) — OpenAI-compatible at https://api.moonshot.ai/v1 (KIMI_API_KEY). Newest first.
119
+ { canonical: "kimi-k2-0711-preview", provider: "kimi", providerModel: "kimi-k2-0711-preview", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false, releaseDate: "2025-07" },
120
+ { canonical: "kimi-thinking-preview", provider: "kimi", providerModel: "kimi-thinking-preview", contextTokens: 128_000, maxOutputTokens: 32_000, thinking: FULL, images: true, releaseDate: "2025-05" },
121
+ { canonical: "kimi-latest", provider: "kimi", providerModel: "kimi-latest", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: true, releaseDate: "2025-02" },
122
+ { canonical: "moonshot-v1-128k", provider: "kimi", providerModel: "moonshot-v1-128k", contextTokens: 128_000, maxOutputTokens: 16_384, thinking: [], images: false, releaseDate: "2024-01" },
96
123
  // Kimi Code (Moonshot coding subscription, device-code OAuth) — Anthropic-compatible
97
124
  // at https://api.kimi.com/coding. Provider-qualified (`kimi/…`) to avoid canonical
98
125
  // collisions (kimi-k2.5 also exists under tencent). gjc catalog parity: kimi-code.
99
- { canonical: "kimi-for-coding", provider: "kimi", providerModel: "kimi/kimi-for-coding", contextTokens: 262_144, maxOutputTokens: 32_000, thinking: FULL, images: true, company: "Moonshot (Kimi Code)" },
100
- { canonical: "kimi-k2.7-code", provider: "kimi", providerModel: "kimi/kimi-k2.7-code", contextTokens: 262_144, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Moonshot (Kimi Code)" },
101
- { canonical: "kimi/kimi-k2.5", provider: "kimi", providerModel: "kimi/kimi-k2.5", contextTokens: 262_144, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Moonshot (Kimi Code)" },
102
- { canonical: "kimi/kimi-k2-turbo-preview", provider: "kimi", providerModel: "kimi/kimi-k2-turbo-preview", contextTokens: 262_144, maxOutputTokens: 32_000, thinking: FULL, images: false, company: "Moonshot (Kimi Code)" },
103
- { canonical: "kimi/kimi-k2", provider: "kimi", providerModel: "kimi/kimi-k2", contextTokens: 262_144, maxOutputTokens: 262_144, thinking: [], images: false, company: "Moonshot (Kimi Code)" },
104
- // Google
105
- { canonical: "gemini-1.5-pro", provider: "gemini", providerModel: "gemini-1.5-pro", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
106
- { canonical: "gemini-2.0-flash", provider: "gemini", providerModel: "gemini-2.0-flash", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true },
107
- { canonical: "gemini-2.5-flash", provider: "gemini", providerModel: "gemini-2.5-flash", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true },
108
- { canonical: "gemini-2.5-pro", provider: "gemini", providerModel: "gemini-2.5-pro", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true },
126
+ // Newest first.
127
+ { canonical: "kimi-k2.7-code", provider: "kimi", providerModel: "kimi/kimi-k2.7-code", contextTokens: 262_144, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Moonshot (Kimi Code)", releaseDate: "2026-06" },
128
+ { canonical: "kimi-for-coding", provider: "kimi", providerModel: "kimi/kimi-for-coding", contextTokens: 262_144, maxOutputTokens: 32_000, thinking: FULL, images: true, company: "Moonshot (Kimi Code)", releaseDate: "2026-01" }, // [INFERENCE] alias-registration date approximated from Kimi CLI's 2026-01 launch — no primary-source changelog entry found
129
+ { canonical: "kimi/kimi-k2.5", provider: "kimi", providerModel: "kimi/kimi-k2.5", contextTokens: 262_144, maxOutputTokens: 65_536, thinking: FULL, images: true, company: "Moonshot (Kimi Code)", releaseDate: "2026-01" },
130
+ { canonical: "kimi/kimi-k2-turbo-preview", provider: "kimi", providerModel: "kimi/kimi-k2-turbo-preview", contextTokens: 262_144, maxOutputTokens: 32_000, thinking: FULL, images: false, company: "Moonshot (Kimi Code)", releaseDate: "2025-08" },
131
+ { canonical: "kimi/kimi-k2", provider: "kimi", providerModel: "kimi/kimi-k2", contextTokens: 262_144, maxOutputTokens: 262_144, thinking: [], images: false, company: "Moonshot (Kimi Code)", releaseDate: "2025-07" },
132
+ // Google (newest first: gemini-3.1-pro > gemini-3-* > gemini-2.5-* > gemini-2.0-flash > gemini-1.5-pro)
133
+ { canonical: "gemini-3.1-pro-high", provider: "gemini", providerModel: "gemini-3.1-pro-high", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, releaseDate: "2026-02" },
134
+ { canonical: "gemini-3.1-pro-low", provider: "gemini", providerModel: "gemini-3.1-pro-low", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, releaseDate: "2026-02" },
135
+ { canonical: "gemini-3-flash", provider: "gemini", providerModel: "gemini-3-flash", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, releaseDate: "2025-12" },
136
+ { canonical: "gemini-3-pro-high", provider: "gemini", providerModel: "gemini-3-pro-high", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, releaseDate: "2025-11" },
137
+ { canonical: "gemini-3-pro-low", provider: "gemini", providerModel: "gemini-3-pro-low", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: FULL, images: true, releaseDate: "2025-11" },
138
+ { canonical: "gemini-2.5-flash", provider: "gemini", providerModel: "gemini-2.5-flash", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true, releaseDate: "2025-06" },
139
+ { canonical: "gemini-2.5-pro", provider: "gemini", providerModel: "gemini-2.5-pro", contextTokens: 1_000_000, maxOutputTokens: 65_536, thinking: STD, images: true, releaseDate: "2025-03" },
140
+ { canonical: "gemini-2.0-flash", provider: "gemini", providerModel: "gemini-2.0-flash", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true, releaseDate: "2025-02" },
141
+ { canonical: "gemini-1.5-pro", provider: "gemini", providerModel: "gemini-1.5-pro", contextTokens: 1_000_000, maxOutputTokens: 8_192, thinking: [], images: true, releaseDate: "2024-02" },
109
142
  // Google Antigravity / Gemini CLI (Cloud Code Assist) — provider-qualified to avoid
110
- // collisions with public Gemini, Anthropic, and OpenAI/Codex ids.
111
- ...ANTIGRAVITY_MODELS.map((id): CatalogModel => ({
143
+ // collisions with public Gemini, Anthropic, and OpenAI/Codex ids. A hosting layer
144
+ // over the SAME underlying models researched above, not a separate release —
145
+ // releaseDate comes from the [id, date] tuple, not re-derived.
146
+ ...ANTIGRAVITY_MODELS.map(([id, releaseDate]): CatalogModel => ({
112
147
  canonical: `antigravity/${id}`,
113
148
  provider: "antigravity",
114
149
  providerModel: id,
@@ -117,31 +152,30 @@ export const MODEL_CATALOG: readonly CatalogModel[] = [
117
152
  thinking: id.includes("thinking") || id.includes("-high") || id.includes("-low") || id.includes("gemini-3") || id.startsWith("gpt-5") ? FULL : STD,
118
153
  images: !id.includes("gpt-oss"),
119
154
  company: id.includes("claude") ? "Anthropic via Antigravity" : id.includes("gpt") ? "OpenAI via Antigravity" : "Google Antigravity",
155
+ releaseDate,
120
156
  })),
121
- // Tencent
122
- // Tencent — DeepSeek
123
- { canonical: "deepseek-v4-pro", provider: "tencent", providerModel: "deepseek-v4-pro", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
124
- { canonical: "deepseek-v4-pro-202606", provider: "tencent", providerModel: "deepseek-v4-pro-202606", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
125
- { canonical: "deepseek-v4-flash", provider: "tencent", providerModel: "deepseek-v4-flash", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
126
- { canonical: "deepseek-v4-flash-202605", provider: "tencent", providerModel: "deepseek-v4-flash-202605", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
127
- { canonical: "deepseek-v3.2", provider: "tencent", providerModel: "deepseek-v3.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
128
- // Tencent — MiniMax
129
- { canonical: "minimax-m3", provider: "tencent", providerModel: "minimax-m3", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
130
- { canonical: "minimax-m2.7", provider: "tencent", providerModel: "minimax-m2.7", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
131
- { canonical: "minimax-m2.5", provider: "tencent", providerModel: "minimax-m2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
132
- // Tencent — Zhipu GLM
133
- { canonical: "glm-5.2", provider: "tencent", providerModel: "glm-5.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
134
- { canonical: "glm-5.1", provider: "tencent", providerModel: "glm-5.1", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
135
- { canonical: "glm-5", provider: "tencent", providerModel: "glm-5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
136
- { canonical: "glm-5-turbo", provider: "tencent", providerModel: "glm-5-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
137
- { canonical: "glm-5v-turbo", provider: "tencent", providerModel: "glm-5v-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: true, company: "Tencent" },
138
- // Tencent — Moonshot Kimi
139
- { canonical: "kimi-k2.6", provider: "tencent", providerModel: "kimi-k2.6", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
140
- { canonical: "kimi-k2.5", provider: "tencent", providerModel: "kimi-k2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent" },
141
- // Tencent — Hunyuan MT
142
- { canonical: "hy-mt2-plus", provider: "tencent", providerModel: "hy-mt2-plus", contextTokens: 128_000, maxOutputTokens: 8192, thinking: STD, images: false, company: "Tencent" },
157
+ // Tencent — hosts the same underlying models under its own cloud brand; dates
158
+ // below are the ORIGINAL developer's release (DeepSeek/MiniMax/Zhipu/Moonshot),
159
+ // not Tencent's hosting-catalog appearance date, except where the id itself pins
160
+ // a Tencent-specific snapshot (the two `-2026XX` suffixed DeepSeek ids). Newest first overall.
161
+ { canonical: "glm-5.2", provider: "tencent", providerModel: "glm-5.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-06" },
162
+ { canonical: "hy-mt2-plus", provider: "tencent", providerModel: "hy-mt2-plus", contextTokens: 128_000, maxOutputTokens: 8192, thinking: STD, images: false, company: "Tencent", releaseDate: "2026-06" },
163
+ { canonical: "deepseek-v4-pro-202606", provider: "tencent", providerModel: "deepseek-v4-pro-202606", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-06" },
164
+ { canonical: "minimax-m3", provider: "tencent", providerModel: "minimax-m3", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-06" },
165
+ { canonical: "deepseek-v4-flash-202605", provider: "tencent", providerModel: "deepseek-v4-flash-202605", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-05" },
166
+ { canonical: "glm-5v-turbo", provider: "tencent", providerModel: "glm-5v-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: true, company: "Tencent", releaseDate: "2026-04" },
167
+ { canonical: "deepseek-v4-pro", provider: "tencent", providerModel: "deepseek-v4-pro", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-04" },
168
+ { canonical: "deepseek-v4-flash", provider: "tencent", providerModel: "deepseek-v4-flash", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-04" },
169
+ { canonical: "kimi-k2.6", provider: "tencent", providerModel: "kimi-k2.6", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-04" },
170
+ { canonical: "minimax-m2.7", provider: "tencent", providerModel: "minimax-m2.7", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-03" },
171
+ { canonical: "glm-5.1", provider: "tencent", providerModel: "glm-5.1", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-03" },
172
+ { canonical: "glm-5-turbo", provider: "tencent", providerModel: "glm-5-turbo", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-03" },
173
+ { canonical: "minimax-m2.5", provider: "tencent", providerModel: "minimax-m2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-02" },
174
+ { canonical: "glm-5", provider: "tencent", providerModel: "glm-5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-02" },
175
+ { canonical: "kimi-k2.5", provider: "tencent", providerModel: "kimi-k2.5", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2026-01" },
176
+ { canonical: "deepseek-v3.2", provider: "tencent", providerModel: "deepseek-v3.2", contextTokens: 128_000, maxOutputTokens: 8192, thinking: FULL, images: false, company: "Tencent", releaseDate: "2025-12" },
143
177
  // Ollama (local)
144
- { canonical: "qwen2.5", provider: "ollama", providerModel: "ollama/qwen2.5:0.5b", contextTokens: 32_768, maxOutputTokens: 8_192, thinking: [], images: false },
178
+ { canonical: "qwen2.5", provider: "ollama", providerModel: "ollama/qwen2.5:0.5b", contextTokens: 32_768, maxOutputTokens: 8_192, thinking: [], images: false, releaseDate: "2024-09" },
145
179
  ];
146
180
 
147
181
  /**
@@ -60,7 +60,11 @@ const ANTIGRAVITY_MODEL_DENYLIST = new Set([
60
60
 
61
61
  function anthropicHeaders(cred: Credential): Record<string, string> {
62
62
  if (cred.kind === "oauth") {
63
- return { authorization: `Bearer ${cred.token}`, "anthropic-version": "2023-06-01", "anthropic-beta": "oauth-2025-04-20" };
63
+ // NOTE: The oauth-2025-04-20 beta is for the messages endpoint, not the models endpoint.
64
+ // The models endpoint does not support this beta, so we omit it here to avoid
65
+ // potential 400/403 errors. The fallback to catalog ensures Anthropic models
66
+ // are always available even if the live endpoint fails.
67
+ return { authorization: `Bearer ${cred.token}`, "anthropic-version": "2023-06-01" };
64
68
  }
65
69
  if (cred.kind === "api_key") {
66
70
  return { "x-api-key": cred.token, "anthropic-version": "2023-06-01" };