diff --git a/src/adapters/cursor/discovery.ts b/src/adapters/cursor/discovery.ts index dd77472723..718323b6a2 100644 --- a/src/adapters/cursor/discovery.ts +++ b/src/adapters/cursor/discovery.ts @@ -214,10 +214,23 @@ export function filterCursorConfiguredModelsByLiveDiscovery - isCursorRouterModelId(model.id) || isCursorModelAvailableForAccount(model.id, liveIds), + !CURSOR_KNOWN_UNCALLABLE_MODEL_IDS.has(model.id) + && (isCursorRouterModelId(model.id) || isCursorModelAvailableForAccount(model.id, liveIds)), ); } +/** + * Models GetUsableModels advertises but whose every Run returns not_found (catalog honesty, + * devlog 260826_cursor_responses_gap 060). Live probes 2026-08-26: cursor/claude-opus-5 failed + * 100% ("Cursor Connect error not_found") while its -fast and -thinking siblings — separate + * wire families — succeed. Quarantined here, in the shared filter, so live, cached, stale, and + * static serving paths all agree. Custom user provider overrides are not routed through this + * canonical seed and stay untouched. + */ +export const CURSOR_KNOWN_UNCALLABLE_MODEL_IDS: ReadonlySet = new Set([ + "claude-opus-5", +]); + export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorModels([ // Context windows and the model lineup mirror Cursor's public models/pricing docs plus the jawcode // SOT (../jawcode/packages/ai/src/models.json, `cursor` provider), which mirrors the real @@ -245,7 +258,8 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM { id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true }, { id: "claude-opus-4-8-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true }, { id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true }, - { id: "claude-opus-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true }, + // claude-opus-5 (bare) removed from the seed: GetUsableModels lists it but every Run returns + // not_found (quarantined via CURSOR_KNOWN_UNCALLABLE_MODEL_IDS; -fast/-thinking families stay). { id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true }, { id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true }, diff --git a/tests/cursor-uncallable-quarantine.test.ts b/tests/cursor-uncallable-quarantine.test.ts new file mode 100644 index 0000000000..99bc3957a0 --- /dev/null +++ b/tests/cursor-uncallable-quarantine.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, test } from "bun:test"; +import { + CURSOR_KNOWN_UNCALLABLE_MODEL_IDS, + CURSOR_STATIC_MODELS, + filterCursorConfiguredModelsByLiveDiscovery, +} from "../src/adapters/cursor/discovery"; + +describe("cursor uncallable-model quarantine (devlog 260826 060)", () => { + test("static seed no longer carries bare claude-opus-5", () => { + expect(CURSOR_STATIC_MODELS.some(model => model.id === "claude-opus-5")).toBe(false); + }); + + test("siblings from other wire families survive", () => { + expect(CURSOR_STATIC_MODELS.some(model => model.id === "claude-opus-5-fast")).toBe(true); + }); + + test("live filter drops quarantined ids even when GetUsableModels lists them", () => { + const configured = [{ id: "claude-opus-5" }, { id: "claude-opus-5-fast" }, { id: "grok-4.6" }]; + const live = ["claude-opus-5-high", "claude-opus-5-high-fast", "grok-4.6-high"]; + const filtered = filterCursorConfiguredModelsByLiveDiscovery(configured, live); + expect(filtered.map(model => model.id)).toEqual(["claude-opus-5-fast", "grok-4.6"]); + }); + + test("quarantine applies with an empty live list too (stale/static degradation path)", () => { + const configured = [{ id: "claude-opus-5" }, { id: "auto" }]; + const filtered = filterCursorConfiguredModelsByLiveDiscovery(configured, []); + expect(filtered.some(model => model.id === "claude-opus-5")).toBe(false); + }); + + test("quarantine set stays narrow", () => { + expect([...CURSOR_KNOWN_UNCALLABLE_MODEL_IDS]).toEqual(["claude-opus-5"]); + }); +});