feat(claude): add Fable 5.2 and Mythos 5.2 to the model catalog (#9233)

Adds Claude Fable 5.2 and Mythos 5.2, available from Claude Code 2.1.321, and defaults both to medium effort to match Claude Code.

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
Co-authored-by: gudcks <84506708+gudcks@users.noreply.github.com>
This commit is contained in:
Jiawen Geng
2026-11-10 01:09:47 +00:00
committed by GitHub
co-authored by Mohamed Boudra gudcks
parent ab64bebe14
commit f9779a14a5
5 changed files with 177 additions and 14 deletions
+10 -6
View File
@@ -62,10 +62,15 @@ const EXPECTED_CLAUDE_MODELS = [
model: "Opus 5",
descriptionFragment: "Previous release",
},
{
id: "claude-fable-5-2",
model: "Fable 5.2",
descriptionFragment: "Most powerful",
},
{
id: "claude-fable-5-1",
model: "Fable 5.1",
descriptionFragment: "Most powerful",
descriptionFragment: "Previous release",
},
{
id: "claude-fable-5",
@@ -199,11 +204,10 @@ function assertClaudeModels(data: ProviderModel[]): void {
);
}
const fable51Index = data.findIndex((model) => model.id === "claude-fable-5-1");
const fable5Index = data.findIndex((model) => model.id === "claude-fable-5");
assert.strictEqual(
fable5Index,
fable51Index + 1,
const fable52Index = data.findIndex((model) => model.id === "claude-fable-5-2");
assert.deepStrictEqual(
data.slice(fable52Index, fable52Index + 3).map((model) => model.id),
["claude-fable-5-2", "claude-fable-5-1", "claude-fable-5"],
"Fable models should stay adjacent and newest-first",
);
assert(
@@ -1614,6 +1614,8 @@ describe("ClaudeAgentSession features", () => {
test.each([
["supported model", "claude-opus-4-8", { type: "disabled" }, undefined],
["Mythos 5.1", "claude-mythos-5-1", { type: "adaptive", display: "summarized" }, "high"],
["Fable 5.2", "claude-fable-5-2", { type: "adaptive", display: "summarized" }, "medium"],
["Mythos 5.2", "claude-mythos-5-2", { type: "adaptive", display: "summarized" }, "medium"],
["Haiku 5.5", "claude-haiku-5-5", { type: "disabled" }, undefined],
["unsupported model", "claude-fable-5", { type: "adaptive", display: "summarized" }, "high"],
["custom model", "openrouter/anthropic/claude-opus-4-8", undefined, undefined],
@@ -5,7 +5,7 @@ export const CLAUDE_FAST_MODE_CONTROL: AgentOptionToggleControl = {
type: "toggle",
key: "fast_mode",
label: "Fast",
description: "Lower latency Opus responses at higher token cost",
description: "Lower latency responses at higher token cost",
tooltip: "Toggle fast mode",
icon: "zap",
};
@@ -58,6 +58,17 @@ export const CLAUDE_MODEL_MANIFEST = [
supportsThinkingDisabled: true,
supportsFastMode: true,
},
{
id: "claude-mythos-5-2",
label: "Mythos 5.2",
requiresDiscovery: true,
description: "Mythos 5.2 · Requires verified access",
minimumClaudeCodeVersion: "2.1.321",
defaultThinkingOptionId: "medium",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsFastMode: true,
},
{
id: "claude-mythos-5-1",
label: "Mythos 5.1",
@@ -66,10 +77,20 @@ export const CLAUDE_MODEL_MANIFEST = [
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
{
id: "claude-fable-5-2",
label: "Fable 5.2",
description: "Fable 5.2 · Most powerful model",
minimumClaudeCodeVersion: "2.1.321",
defaultThinkingOptionId: "medium",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
supportsFastMode: true,
},
{
id: "claude-fable-5-1",
label: "Fable 5.1",
description: "Fable 5.1 · Most powerful model",
description: "Fable 5.1 · Previous release",
contextWindowMaxTokens: 1_000_000,
effortLevels: CLAUDE_EFFORT_LEVELS.xhigh,
},
@@ -61,17 +61,23 @@ async function createClaudeConfigDirWithRawSettings(settings: string): Promise<s
return configDir;
}
function createCatalogClient(claudeCodeVersion = "2.1.293"): ClaudeAgentClient {
function createCatalogClient(claudeCodeVersion = "2.1.321"): ClaudeAgentClient {
return new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => claudeCodeVersion,
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-1",
resolvedModel: "claude-mythos-5-2",
displayName: "Mythos",
description: "",
},
{
value: "claude-mythos-5-1",
resolvedModel: "claude-mythos-5-1",
displayName: "Mythos 5.1",
description: "",
},
],
});
}
@@ -82,7 +88,9 @@ describe("getClaudeModels", () => {
expect(models.map((m) => m.id)).toEqual([
"claude-opus-5-5",
"claude-opus-5",
"claude-mythos-5-2",
"claude-mythos-5-1",
"claude-fable-5-2",
"claude-fable-5-1",
"claude-fable-5",
"claude-fable-5[1m]",
@@ -118,7 +126,9 @@ describe("getClaudeModels", () => {
new Map([
["claude-opus-5-5", 1_000_000],
["claude-opus-5", 1_000_000],
["claude-mythos-5-2", 1_000_000],
["claude-mythos-5-1", 1_000_000],
["claude-fable-5-2", 1_000_000],
["claude-fable-5-1", 1_000_000],
["claude-fable-5", 1_000_000],
["claude-fable-5[1m]", 1_000_000],
@@ -163,6 +173,12 @@ describe("getClaudeModels", () => {
expect(getClaudeModels("2.1.293").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5");
});
it("gates Fable 5.2 on Claude Code 2.1.321 without changing the default", () => {
expect(getClaudeModels("2.1.320").map((model) => model.id)).not.toContain("claude-fable-5-2");
expect(getClaudeModels("2.1.321").map((model) => model.id)).toContain("claude-fable-5-2");
expect(getClaudeModels("2.1.321").find((model) => model.isDefault)?.id).toBe("claude-opus-5-5");
});
it("derives thinking options from model effort capabilities", () => {
const models = new Map(getClaudeModels().map((model) => [model.id, model]));
@@ -232,6 +248,8 @@ describe("getClaudeModels", () => {
["claude-sonnet-5-20260101", true, "high"],
["claude-fable-5", false, "high"],
["claude-fable-5-1", false, "high"],
["claude-fable-5-2", false, "medium"],
["claude-mythos-5-2", false, "medium"],
["claude-haiku-5-5", true, "medium"],
["claude-haiku-5-5-20261007", true, "medium"],
["claude-haiku-4-5", false, undefined],
@@ -284,6 +302,31 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
},
);
it("offers Mythos 5.2 in place of Mythos 5.1 when Claude Code resolves mythos to 5.2", async () => {
const configDir = await createClaudeConfigDir({});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.321",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-2",
displayName: "Mythos",
description: "",
},
],
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.filter((model) => model.id.startsWith("claude-mythos-"))).toEqual([
findClaudeModel("claude-mythos-5-2"),
]);
});
it("preserves the curated catalog and explicitly configured models after discovery failure", async () => {
const configDir = await createClaudeConfigDir({ model: "claude-mythos-5-1" });
const client = new ClaudeAgentClient({
@@ -299,8 +342,8 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
cwd: configDir,
force: false,
});
expect(models.filter((model) => model.id !== "claude-mythos-5-1")).toEqual(
getClaudeModels("2.1.293").filter((model) => model.id !== "claude-mythos-5-1"),
expect(models.filter((model) => !model.id.startsWith("claude-mythos-"))).toEqual(
getClaudeModels("2.1.293").filter((model) => !model.id.startsWith("claude-mythos-")),
);
expect(models.find((model) => model.id === "claude-mythos-5-1")).toEqual({
...findClaudeModel("claude-mythos-5-1"),
@@ -360,7 +403,7 @@ describe("ClaudeAgentClient.fetchCatalog", () => {
});
expect(models.map((model) => model.id)).toEqual(
getClaudeModels("2.1.293")
.filter((model) => model.id !== "claude-mythos-5-1")
.filter((model) => !model.id.startsWith("claude-mythos-"))
.map((model) => model.id),
);
});
@@ -785,6 +828,44 @@ describe("Claude Fable 5.1 catalog", () => {
});
});
describe("Claude Fable 5.2 catalog", () => {
it("offers one 1M model with medium default effort, always-on thinking, and fast mode", () => {
const models = getClaudeModels().filter((model) => model.id.startsWith("claude-fable-5-2"));
expect(models).toHaveLength(1);
expect(models[0]).toMatchObject({
id: "claude-fable-5-2",
label: "Fable 5.2",
contextWindowMaxTokens: 1_000_000,
defaultThinkingOptionId: "medium",
});
expect(models[0].thinkingOptions?.map((option) => option.id)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
]);
expect(claudeManifestModelSupportsFastMode("claude-fable-5-2")).toBe(true);
});
it.each([
"claude-fable-5-2[1m]",
"claude-fable-5-2-20261110",
"claude-fable-5-2-20261110[1m]",
"Fable 5.2",
])("resolves %s to the single catalog entry", (id) => {
expect(findClaudeModel(id)?.id).toBe("claude-fable-5-2");
});
it.each(["claude-fable-5-2", "anthropic/claude-fable-5-2", "us.anthropic.claude-fable-5-2-v1:0"])(
"keeps runtime model %s from collapsing onto Fable 5",
(id) => {
expect(normalizeClaudeRuntimeModelId(id)).toBe("claude-fable-5-2");
},
);
});
describe("Claude Opus 5.5 catalog", () => {
it("offers one Opus 5.5 entry with a 1M context window", () => {
const opus55Models = getClaudeModels()
@@ -920,7 +1001,7 @@ describe("claudeManifestModelSupportsFastMode", () => {
describe("Claude Mythos 5.1 catalog", () => {
it("offers one 1M model with high default effort and always-on thinking", () => {
const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos"));
const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos-5-1"));
expect(models).toHaveLength(1);
expect(models[0]).toMatchObject({
id: "claude-mythos-5-1",
@@ -965,6 +1046,61 @@ describe("Claude Mythos 5.1 catalog", () => {
);
});
describe("Claude Mythos 5.2 catalog", () => {
it("stays hidden on Claude Code older than 2.1.321 even when advertised", async () => {
const configDir = await createClaudeConfigDir({});
const client = new ClaudeAgentClient({
logger: createTestLogger(),
resolveVersion: async () => "2.1.320",
runtimeSettings: { env: { CLAUDE_CONFIG_DIR: configDir } },
discoverModels: async () => [
{
value: "mythos",
resolvedModel: "claude-mythos-5-2",
displayName: "Mythos",
description: "",
},
],
});
const { models } = await client.fetchCatalog({
scope: "workspace",
cwd: configDir,
force: false,
});
expect(models.some((model) => model.id === "claude-mythos-5-2")).toBe(false);
});
it("offers one 1M model with medium default effort, always-on thinking, and fast mode", () => {
const models = getClaudeModels().filter((model) => model.id.startsWith("claude-mythos-5-2"));
expect(models).toHaveLength(1);
expect(models[0]).toMatchObject({
id: "claude-mythos-5-2",
label: "Mythos 5.2",
contextWindowMaxTokens: 1_000_000,
defaultThinkingOptionId: "medium",
});
expect(models[0].thinkingOptions?.map((option) => option.id)).toEqual([
"low",
"medium",
"high",
"xhigh",
"max",
CLAUDE_ULTRACODE_THINKING_OPTION_ID,
]);
expect(claudeManifestModelSupportsFastMode("claude-mythos-5-2")).toBe(true);
});
it.each(["anthropic/claude-mythos-5-2", "us.anthropic.claude-mythos-5-2-v1:0"])(
"normalizes runtime identity without granting first-party capabilities: %s",
(id) => {
expect(normalizeClaudeRuntimeModelId(id)).toBe("claude-mythos-5-2");
expect(normalizeClaudeManifestModelId(id)).toBeNull();
},
);
});
describe("Claude model discovery process", () => {
it("does not send a prompt or persist a session and closes after discovery", async () => {
let closed = false;