diff --git a/src/adapters/cursor/catalog.ts b/src/adapters/cursor/catalog.ts index 9c7fa8b2bb1..f753f24e741 100644 --- a/src/adapters/cursor/catalog.ts +++ b/src/adapters/cursor/catalog.ts @@ -106,6 +106,36 @@ export const CURSOR_CAPABILITIES: Record = { thinking: { levels: FULL, order: T }, }, }, + // 260902 preemptive: Claude Fable 5.1 seeded ahead of Cursor's lineup update, mirroring + // claude-fable-5 (same 1M window and full effort ladder). Cursor has spelled Claude ids + // both Anthropic-style (`claude-opus-4-7`, thinking-then-effort) and version-first + // (`claude-4.6-opus`, effort-then-thinking), so all three plausible spellings are seeded; + // the live GetUsableModels filter drops whichever the roster does not expose. Collapse to + // the one real spelling once it is observed. + "claude-fable-5-1": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: T }, + }, + }, + "claude-fable-5.1": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: T }, + }, + }, + "claude-5.1-fable": { + window: CONTEXT_1M, + defaultVariant: "thinking", + variants: { + regular: { levels: FULL }, + thinking: { levels: FULL, order: E }, + }, + }, "claude-sonnet-5": { window: CONTEXT_1M, defaultVariant: "thinking", diff --git a/src/adapters/cursor/discovery.ts b/src/adapters/cursor/discovery.ts index 1b0709fd828..93819289e77 100644 --- a/src/adapters/cursor/discovery.ts +++ b/src/adapters/cursor/discovery.ts @@ -30,6 +30,8 @@ export function inferCursorContextWindow(modelId: string): number { if (id.includes("1m")) return CONTEXT_1M; if (id.startsWith("gemini-")) return CONTEXT_1M; if (id === "glm-5.3" || id === "glm-5.2") return CONTEXT_1M; + // 260902: every Fable is a 1M model; catch live spellings the seed does not carry. + if (id.includes("fable")) return CONTEXT_1M; if (id.startsWith("gpt-5.6-")) return CONTEXT_1M; if (id.startsWith("gpt-5") || id === "gpt-5-codex") return CONTEXT_272K; if (id.startsWith("grok-4.5") || id.startsWith("grok-4.6")) return 500_000; @@ -291,6 +293,11 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM // quarantined regular wire id for the bare slug). { id: "claude-opus-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true }, { id: "claude-fable-5", contextWindow: CONTEXT_1M, supportsReasoningEffort: true }, + // 260902 preemptive: Fable 5.1 seeded ahead of Cursor's lineup update (mirrors fable-5) under + // the three spellings Cursor has used for Claude ids; see CURSOR_CAPABILITIES. + { id: "claude-fable-5-1", contextWindow: CONTEXT_1M, supportsReasoningEffort: true }, + { id: "claude-fable-5.1", contextWindow: CONTEXT_1M, supportsReasoningEffort: true }, + { id: "claude-5.1-fable", contextWindow: CONTEXT_1M, supportsReasoningEffort: true }, { id: "composer-1", contextWindow: CONTEXT_200K }, { id: "composer-2.5", contextWindow: CONTEXT_200K }, diff --git a/src/adapters/cursor/effort-map.ts b/src/adapters/cursor/effort-map.ts index 29525b41904..371e2f40beb 100644 --- a/src/adapters/cursor/effort-map.ts +++ b/src/adapters/cursor/effort-map.ts @@ -23,6 +23,11 @@ const CURSOR_MODEL_EFFORT_TIERS: Record = { // max is always the top tier (canonical order: low < medium < high < xhigh < max), confirmed // against Anthropic's effort ladder docs and Cursor's live model lineup. "claude-fable-5": ["low", "medium", "high", "xhigh", "max"], + // 260902 preemptive: Fable 5.1 seeded ahead of Cursor's lineup update (mirrors fable-5) under + // the three spellings Cursor has used for Claude ids. + "claude-fable-5-1": ["low", "medium", "high", "xhigh", "max"], + "claude-fable-5.1": ["low", "medium", "high", "xhigh", "max"], + "claude-5.1-fable": ["low", "medium", "high", "xhigh", "max"], "claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"], // Opus Fast tiers from the 260822 GetUsableModels dump (devlog .../300): the wire // exposes {base-without-fast}-{effort}-fast only; suffix derivation at the bottom of @@ -49,6 +54,9 @@ const CURSOR_MODEL_EFFORT_TIERS: Record = { "claude-opus-4-7-thinking-fast": ["low", "medium", "high", "xhigh", "max"], "claude-sonnet-5-thinking": ["low", "medium", "high", "xhigh", "max"], "claude-fable-5-thinking": ["low", "medium", "high", "xhigh", "max"], + "claude-fable-5-1-thinking": ["low", "medium", "high", "xhigh", "max"], + "claude-fable-5.1-thinking": ["low", "medium", "high", "xhigh", "max"], + "claude-5.1-fable-thinking": ["low", "medium", "high", "xhigh", "max"], "claude-4.6-opus-thinking": ["high", "max"], "claude-4.5-opus-thinking": ["high"], "claude-4.6-sonnet-thinking": ["medium"], @@ -113,6 +121,9 @@ const CURSOR_THINKING_FAMILIES: Readonly { expect(ids).toContain("glm-5.2"); expect(ids).toContain("kimi-k2.7-code"); expect(ids).toContain("kimi-k3"); + // 260902 preemptive seed: Fable 5.1 registered ahead of Cursor's lineup update, at 1M, + // under the three spellings Cursor has used for Claude ids. + for (const spelling of ["claude-fable-5-1", "claude-fable-5.1", "claude-5.1-fable"]) { + expect(ids).toContain(spelling); + expect(cursorModelContextWindows(CURSOR_STATIC_MODELS)[spelling]).toBe(1_000_000); + } + // Any live Fable spelling the seed does not carry still infers a 1M window. + expect(inferCursorContextWindow("claude-fable-6")).toBe(1_000_000); // Umbrella merge (devlog 260828): fast duplicate rows folded into bases. expect(ids).not.toContain("claude-opus-4-7-fast"); // 260709 refresh: stale ids dropped from the static seed (cursor.com docs); gpt-5.5-extra diff --git a/tests/cursor-effort-suffix.test.ts b/tests/cursor-effort-suffix.test.ts index ae7567f2603..c509e2fc564 100644 --- a/tests/cursor-effort-suffix.test.ts +++ b/tests/cursor-effort-suffix.test.ts @@ -244,6 +244,10 @@ describe("#2569 Cursor explicit-thinking variants", () => { ["claude-opus-4-8-thinking-fast", "xhigh", "claude-opus-4-8-thinking-xhigh-fast"], ["claude-sonnet-5-thinking", "medium", "claude-sonnet-5-thinking-medium"], ["claude-fable-5-thinking", "xhigh", "claude-fable-5-thinking-xhigh"], + ["claude-fable-5-1-thinking", "xhigh", "claude-fable-5-1-thinking-xhigh"], + ["claude-fable-5.1-thinking", "xhigh", "claude-fable-5.1-thinking-xhigh"], + // Version-first spelling follows the 4.x families: marker at the END. + ["claude-5.1-fable-thinking", "max", "claude-5.1-fable-max-thinking"], // The marker moves to the END for these families. ["claude-4.6-opus-thinking", "max", "claude-4.6-opus-max-thinking"], ["claude-4.5-opus-thinking", "high", "claude-4.5-opus-high-thinking"], diff --git a/tests/usage-cost.test.ts b/tests/usage-cost.test.ts index bf15ab84702..f6bf02d482d 100644 --- a/tests/usage-cost.test.ts +++ b/tests/usage-cost.test.ts @@ -195,6 +195,11 @@ describe("resolveMatchedPrice", () => { expect(price?.sourceRef).toContain("0.025x"); } expect(resolveMatchedPrice("anthropic-pb51d9b", "claude-fable-5-1")?.cost4).toEqual(COST4); + // Cursor seeds the id preemptively under three spellings and jawcode has no row, so + // each carries its own (derived) overlay rather than falling through to null. + for (const spelling of ["claude-fable-5-1", "claude-fable-5.1", "claude-5.1-fable"]) { + expect(resolveMatchedPrice("cursor", spelling), spelling).toMatchObject({ cost4: COST4, source: "expected", status: "verified-derived" }); + } // The cheaper cache-hit rate must not leak onto Fable 5, which stays at 0.1x. expect(resolveMatchedPrice("anthropic", "claude-fable-5")?.cost4.cacheRead).toBe(1); }); @@ -292,13 +297,16 @@ describe("resolveMatchedPrice", () => { expect(resolveMatchedPrice("openrouter", "anthropic-claude-3.5-sonnet")).toBeNull(); }); - test("16. shipped overlay membership: 58 keys, including Fable 5.1, Opus 5 and compatibility prices", () => { - expect(EXPECTED_PRICE_OVERLAYS.length).toBe(58); + test("16. shipped overlay membership: 61 keys, including Fable 5.1, Opus 5 and compatibility prices", () => { + expect(EXPECTED_PRICE_OVERLAYS.length).toBe(61); expect(EXPECTED_PRICE_OVERLAYS.some(row => row.status === "unverified")).toBe(false); const keys = new Set(EXPECTED_PRICE_OVERLAYS.map(row => `${row.provider}/${row.modelId}`)); for (const expected of [ "anthropic/claude-fable-5-1", "anthropic-apikey/claude-fable-5-1", + "cursor/claude-fable-5-1", + "cursor/claude-fable-5.1", + "cursor/claude-5.1-fable", "anthropic/claude-opus-5", "cursor/claude-opus-5", "kiro/claude-opus-5",