Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/ai/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

### Fixed

- Fixed Cursor Claude Fable 5.1 variants remaining as ten separate models with a 200k fallback window: they now group into plain and thinking identities with the full effort ladder and a 1M catalog window, while server-observed context limits still take precedence ([#1613](https://github.com/code-yeongyu/senpi/pull/1613) by [@thisisjun786](https://github.com/thisisjun786)).

### Removed

## [2026.9.12-2] - 2026-09-12
Expand Down
20 changes: 20 additions & 0 deletions packages/ai/src/changes.md
Original file line number Diff line number Diff line change
@@ -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`.

## Cursor context ceilings come from the server, not the capability table (2026-09-12)

### What changed
Expand Down
62 changes: 61 additions & 1 deletion packages/ai/src/cursor/cursor-variant-aliases.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
Comment on lines +133 to +137

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve observed limits while regrouping Fable aliases

When upgrading a user who previously ran a raw Fable 5.1 variant, this new alias makes regroupStoredCursorModels replace that raw ID with the grouped target, but cursor-context-limits.json remains keyed by the raw ID and entryToModel queries only the grouped entry.id. Consequently, a persisted server limit such as 200k for claude-fable-5-1-medium is ignored and the migrated model is advertised as 1M, so a resumed large turn can exceed the account's actual ceiling before another checkpoint is received; migrate or consult the representative/legacy alias limits during regrouping.

Useful? React with 👍 / 👎.

},
"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"
Comment on lines +163 to +167

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep raw variants in the Cursor CLI cache

When a Cursor CLI probe includes these new variants, parseCursorAgentModelsListing groups them and writeCache saves only the resulting grouped configs. On the next launch, parseCachedCatalog feeds the grouped IDs back through normalizeEntries; because those target IDs are not alias keys, both entries become non-reasoning models without cursorReasoning or upstreamModelId, and resolveCursorSelectionDescriptor sends the bare claude-fable-5-1[-thinking] ID that Cursor Run rejects with not_found. Thus these identities work immediately after a probe but fail while the 24-hour cache is fresh; cache the raw variants or deserialize the normalized configs without regrouping them.

Useful? React with 👍 / 👎.

},
"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",
Expand Down
6 changes: 6 additions & 0 deletions packages/ai/src/cursor/model-capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, CursorModelCapability> = {
"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"),
Expand Down
17 changes: 17 additions & 0 deletions packages/ai/test/cursor-model-capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
});
Expand All @@ -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);
Expand Down
38 changes: 38 additions & 0 deletions packages/ai/test/cursor-model-grouping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
]
}
]
1 change: 1 addition & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
### Fixed

- Fixed native prompt-preset matching for Devin SWE-2 effort variants so `swe-2-high`, `swe-2-max`, `swe-2-low`, and `swe-2-high-lite` resolve to the Kimi K3 preset; the rejected bare `swe-2` id remains unmatched.
- Fixed Cursor Claude Fable 5.1 variants remaining as ten separate models with a 200k fallback window: they now group into plain and thinking identities with the full effort ladder and a 1M catalog window, while server-observed context limits still take precedence ([#1613](https://github.com/code-yeongyu/senpi/pull/1613) by [@thisisjun786](https://github.com/thisisjun786)).

### Removed

Expand Down