From c9553a3d80e1001184afe11b64ec28367ee2a5ec Mon Sep 17 00:00:00 2001 From: thisisjun786 <259586770+thisisjun786@users.noreply.github.com> Date: Sat, 12 Sep 2026 03:59:35 +0900 Subject: [PATCH] fix(ai): add claude-fable-5-1 to the cursor capability table and variant aliases Cursor began serving `claude-fable-5-1-{low,medium,high,xhigh,max}` and the matching `-thinking-` variants after the 2026-08-18 capture the static table was built from. With no `CURSOR_MODEL_CAPABILITIES` entry, `normalizeCursorCatalog` left the ten variants ungrouped and stamped each with the 200000 unknown-family fallback, even though `GetUsableModels` names them "Claude Fable 5.1 1M ..."; with no alias, `regroupStoredCursorModels` could never fold an existing store into the grouped shape. The result was ten separate 200k picker entries and compaction firing at 200k on a 1M model. - `claude-fable-5-1` takes the same contract as `claude-fable-5`: 1M window, 1M maxWindow, `300k` default context, `1m` request context, `low..max` ladder. - ten aliases map the variants onto `claude-fable-5-1` / `claude-fable-5-1-thinking`. - the raw 2026-09-11 `GetUsableModels` entries are committed as a fixture and drive new grouping and resolution tests. --- packages/ai/src/changes.md | 20 ++++ .../ai/src/cursor/cursor-variant-aliases.json | 62 +++++++++- packages/ai/src/cursor/model-capabilities.ts | 6 + .../ai/test/cursor-model-capabilities.test.ts | 17 +++ .../ai/test/cursor-model-grouping.test.ts | 38 ++++++ ...able-models-claude-fable-5-1-20260911.json | 112 ++++++++++++++++++ 6 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 packages/ai/test/fixtures/cursor-usable-models-claude-fable-5-1-20260911.json diff --git a/packages/ai/src/changes.md b/packages/ai/src/changes.md index cbf8763f8b..93980975b4 100644 --- a/packages/ai/src/changes.md +++ b/packages/ai/src/changes.md @@ -1,3 +1,23 @@ +## Cursor claude-fable-5-1 capability entry and variant aliases (2026-09-11) + +### What changed + +- `packages/ai/src/cursor/model-capabilities.ts`: `CURSOR_MODEL_CAPABILITIES` gains `claude-fable-5-1` with the same contract as `claude-fable-5` (1M window, 1M maxWindow, `300k` default context, `1m` request context, `low..max` effort ladder). +- `packages/ai/src/cursor/cursor-variant-aliases.json`: ten new aliases (`claude-fable-5-1-{low,medium,high,xhigh,max}` and `claude-fable-5-1-thinking-{low,medium,high,xhigh,max}`) resolving to the `claude-fable-5-1` / `claude-fable-5-1-thinking` identities; `source` records the 2026-09-11 capture. +- `packages/ai/test/fixtures/cursor-usable-models-claude-fable-5-1-20260911.json`: the ten raw `GetUsableModels` entries as Cursor served them on 2026-09-11, every display name carrying the "1M" label. + +### Why + +- Cursor started serving `claude-fable-5-1-*` after the 2026-08-18 capture the table and alias file were built from. Without a capability entry, `normalizeCursorCatalog` kept the ten variants ungrouped and stamped each with the 200000 unknown-family fallback, discarding the 1M window discovery had already recovered from the display name, so compaction fired at 200k on a 1M model; without an alias, `regroupStoredCursorModels` could never fold an existing store into the grouped shape, and the ten variants stayed ten separate 200k entries in the picker instead of one plain and one thinking identity with an effort ladder. + +### Why an extension could not handle it + +- The capability table and alias map are the core provider data both Cursor transports and the store migration read; there is no extension hook between discovery and the persisted catalog. + +### Expected merge conflict zones + +- `model-capabilities.ts` family table, `cursor-variant-aliases.json` Claude block, the expected-window map in `test/cursor-model-capabilities.test.ts`. + ## Devin Cascade model transport (2026-09-12) ### What changed diff --git a/packages/ai/src/cursor/cursor-variant-aliases.json b/packages/ai/src/cursor/cursor-variant-aliases.json index 5012f4dc47..296e2ba213 100644 --- a/packages/ai/src/cursor/cursor-variant-aliases.json +++ b/packages/ai/src/cursor/cursor-variant-aliases.json @@ -1,6 +1,6 @@ { "$schema": "cursor-variant-aliases/v1", - "source": "union of GetUsableModels live capture (204 ids) and cursor-agent CLI listing capture (203 ids), 2026-08-18", + "source": "union of GetUsableModels live capture (204 ids) and cursor-agent CLI listing capture (203 ids), 2026-08-18; claude-fable-5-1 variants from GetUsableModels live capture (223 ids), 2026-09-11", "aliases": { "claude-4-sonnet": { "targetId": "claude-4-sonnet", @@ -130,6 +130,66 @@ "legacyVariantId": "claude-fable-5-thinking-xhigh", "encoding": "legacy-variant" }, + "claude-fable-5-1-high": { + "targetId": "claude-fable-5-1", + "level": "high", + "legacyVariantId": "claude-fable-5-1-high", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-low": { + "targetId": "claude-fable-5-1", + "level": "low", + "legacyVariantId": "claude-fable-5-1-low", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-max": { + "targetId": "claude-fable-5-1", + "level": "max", + "legacyVariantId": "claude-fable-5-1-max", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-medium": { + "targetId": "claude-fable-5-1", + "level": "medium", + "legacyVariantId": "claude-fable-5-1-medium", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-xhigh": { + "targetId": "claude-fable-5-1", + "level": "xhigh", + "legacyVariantId": "claude-fable-5-1-xhigh", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-thinking-high": { + "targetId": "claude-fable-5-1-thinking", + "level": "high", + "legacyVariantId": "claude-fable-5-1-thinking-high", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-thinking-low": { + "targetId": "claude-fable-5-1-thinking", + "level": "low", + "legacyVariantId": "claude-fable-5-1-thinking-low", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-thinking-max": { + "targetId": "claude-fable-5-1-thinking", + "level": "max", + "legacyVariantId": "claude-fable-5-1-thinking-max", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-thinking-medium": { + "targetId": "claude-fable-5-1-thinking", + "level": "medium", + "legacyVariantId": "claude-fable-5-1-thinking-medium", + "encoding": "legacy-variant" + }, + "claude-fable-5-1-thinking-xhigh": { + "targetId": "claude-fable-5-1-thinking", + "level": "xhigh", + "legacyVariantId": "claude-fable-5-1-thinking-xhigh", + "encoding": "legacy-variant" + }, "claude-opus-4-7-high": { "targetId": "claude-opus-4-7", "level": "high", diff --git a/packages/ai/src/cursor/model-capabilities.ts b/packages/ai/src/cursor/model-capabilities.ts index e3631f74cb..057e3922c4 100644 --- a/packages/ai/src/cursor/model-capabilities.ts +++ b/packages/ai/src/cursor/model-capabilities.ts @@ -77,9 +77,15 @@ function gpt( * local-ignore/qa-evidence/20260818-cursor-reasoning-levels/available-models-catalog.json). * `GetUsableModels` carries no window or parameter field, so this table is the * authoritative source; see .omo/plans/cursor-reasoning-levels.md §3.1/§6. + * + * `claude-fable-5-1` comes from the 2026-09-11 `GetUsableModels` capture + * (test/fixtures/cursor-usable-models-claude-fable-5-1-20260911.json): the same + * ten `-{level}` / `-thinking-{level}` variant ids as `claude-fable-5`, every + * display name carrying the "1M" label, so it takes the same 1M contract. */ export const CURSOR_MODEL_CAPABILITIES: Record = { "claude-fable-5": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"), + "claude-fable-5-1": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"), "claude-sonnet-5": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"), "claude-opus-4-7": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"), "claude-opus-4-8": claude(1000000, 1000000, ["low", "medium", "high", "xhigh", "max"], "300k"), diff --git a/packages/ai/test/cursor-model-capabilities.test.ts b/packages/ai/test/cursor-model-capabilities.test.ts index 344fb67fba..36d669ff1f 100644 --- a/packages/ai/test/cursor-model-capabilities.test.ts +++ b/packages/ai/test/cursor-model-capabilities.test.ts @@ -6,6 +6,7 @@ import { parseCursorVariantId, } from "../src/cursor/model-capabilities.ts"; import fixture from "./fixtures/cursor-usable-models-20260818.json" with { type: "json" }; +import fable51Fixture from "./fixtures/cursor-usable-models-claude-fable-5-1-20260911.json" with { type: "json" }; /** * C1 — capability table and context windows. @@ -35,6 +36,7 @@ describe("cursor model capabilities", () => { "claude-4.6-sonnet": 1000000, "claude-4.6-opus": 1000000, "claude-fable-5": 1000000, + "claude-fable-5-1": 1000000, "claude-sonnet-5": 1000000, "claude-opus-4-7": 1000000, "claude-opus-4-8": 1000000, @@ -65,6 +67,8 @@ describe("cursor model capabilities", () => { it("resolves a variant id to its base capability with the same window", () => { expect(getCursorBaseIdForVariant("kimi-k3-max")).toBe("kimi-k3"); expect(getCursorBaseIdForVariant("claude-fable-5-thinking-xhigh")).toBe("claude-fable-5"); + expect(getCursorBaseIdForVariant("claude-fable-5-1-thinking-xhigh")).toBe("claude-fable-5-1"); + expect(getCursorBaseIdForVariant("claude-fable-5-1-max")).toBe("claude-fable-5-1"); expect(getCursorBaseIdForVariant("claude-opus-4-7-thinking-high-fast")).toBe("claude-opus-4-7"); expect(getCursorCapabilityForBase(getCursorBaseIdForVariant("kimi-k3-max")!)?.window).toBe(1048576); }); @@ -82,6 +86,19 @@ describe("cursor model capabilities", () => { expect([...uncovered].sort()).toEqual(["default"]); }); + it("gives claude-fable-5-1 the same 1M contract as claude-fable-5", () => { + const fable = getCursorCapabilityForBase("claude-fable-5"); + const fable51 = getCursorCapabilityForBase("claude-fable-5-1"); + expect(fable51).toEqual(fable); + expect(fable51?.window).toBe(1000000); + expect(fable51?.requestContext).toBe("1m"); + for (const entry of fable51Fixture) { + const parsed = parseCursorVariantId(entry.id); + expect(parsed.baseId, entry.id).toBe("claude-fable-5-1"); + expect(getCursorBaseIdForVariant(entry.id), entry.id).toBe("claude-fable-5-1"); + } + }); + it("marks evidence provenance for every entry", () => { for (const [id, cap] of Object.entries(CURSOR_MODEL_CAPABILITIES)) { expect(["available-models", "cli-live", "suffix-only"], `${id} evidence`).toContain(cap.evidence); diff --git a/packages/ai/test/cursor-model-grouping.test.ts b/packages/ai/test/cursor-model-grouping.test.ts index 287e053776..0f061783ab 100644 --- a/packages/ai/test/cursor-model-grouping.test.ts +++ b/packages/ai/test/cursor-model-grouping.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { normalizeCursorCatalog, parseCursorVariantId } from "../src/cursor/catalog-grouping.ts"; import { getCursorVariantAlias } from "../src/cursor/model-capabilities.ts"; import fixture from "./fixtures/cursor-usable-models-20260818.json" with { type: "json" }; +import fable51Fixture from "./fixtures/cursor-usable-models-claude-fable-5-1-20260911.json" with { type: "json" }; const ALL_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const; @@ -114,6 +115,43 @@ describe("normalizeCursorCatalog (golden 204-id live fixture)", () => { }); }); +describe("normalizeCursorCatalog (claude-fable-5-1 live capture, 2026-09-11)", () => { + function normalizedFable51() { + return normalizeCursorCatalog( + fable51Fixture.map((entry) => ({ + id: entry.id, + name: entry.name, + input: entry.input as ("text" | "image")[], + cursorMaxMode: entry.cursorMaxMode, + })), + ); + } + + it("collapses the ten raw variants into a plain and a thinking identity", () => { + const out = normalizedFable51(); + expect(out.map((entry) => entry.id).sort()).toEqual(["claude-fable-5-1", "claude-fable-5-1-thinking"]); + const plain = out.find((entry) => entry.id === "claude-fable-5-1"); + const thinking = out.find((entry) => entry.id === "claude-fable-5-1-thinking"); + expect(plain?.thinkingMode).toBe(false); + expect(thinking?.thinkingMode).toBe(true); + for (const identity of [plain, thinking]) { + expect(identity?.reasoning).toBe(true); + expect(identity?.capabilityId).toBe("claude-fable-5-1"); + for (const level of ["low", "medium", "high", "xhigh", "max"] as const) { + expect(identity?.thinkingLevelMap?.[level], `${identity?.id} ${level}`).toBe(level); + } + } + }); + + it("advertises the 1M window instead of the 200k unknown-family fallback", () => { + for (const entry of normalizedFable51()) { + expect(entry.window, entry.id).toBe(1000000); + expect(entry.maxWindow, entry.id).toBe(1000000); + expect(entry.name, entry.id).toContain("1M"); + } + }); +}); + describe("parseCursorVariantId", () => { it("parses every fixture id and round-trips via originalId", () => { for (const entry of fixture) { diff --git a/packages/ai/test/fixtures/cursor-usable-models-claude-fable-5-1-20260911.json b/packages/ai/test/fixtures/cursor-usable-models-claude-fable-5-1-20260911.json new file mode 100644 index 0000000000..3ad7a815b9 --- /dev/null +++ b/packages/ai/test/fixtures/cursor-usable-models-claude-fable-5-1-20260911.json @@ -0,0 +1,112 @@ +[ + { + "id": "claude-fable-5-1-high", + "name": "Claude Fable 5.1 1M (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-low", + "name": "Claude Fable 5.1 1M Low (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-max", + "name": "Claude Fable 5.1 1M Max (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-medium", + "name": "Claude Fable 5.1 1M Medium (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-thinking-high", + "name": "Claude Fable 5.1 1M Thinking (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-thinking-low", + "name": "Claude Fable 5.1 1M Low Thinking (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-thinking-max", + "name": "Claude Fable 5.1 1M Max Thinking (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-thinking-medium", + "name": "Claude Fable 5.1 1M Medium Thinking (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-thinking-xhigh", + "name": "Claude Fable 5.1 1M Extra High Thinking (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + }, + { + "id": "claude-fable-5-1-xhigh", + "name": "Claude Fable 5.1 1M Extra High (NO ZDR)", + "reasoning": false, + "contextWindow": 1000000, + "cursorMaxMode": false, + "input": [ + "text", + "image" + ] + } +]