feat(cursor): exclusive live listing + verbatim AgentRun model ids (#9911)

* feat(cursor): prefer live synced catalog for listing and Test All

When an active synced Cursor catalog exists, list only live models plus
injected auto routers (and customs). Keep the static registry as offline
fallback.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cursor): send live-catalog model ids verbatim on AgentRun

Skip #7289 effort/reasoning splits when the exact id is in the active synced
Cursor catalog so AgentRun does not rewrite flattened live ids into missing
bases that return AI Model Not Found. Also wires auto-cost/balance/intelligence
to default + optimization for the injected routers.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: yansigit <yansigit@users.noreply.github.com>
This commit is contained in:
SB Yoon
2026-08-10 00:46:14 -06:00
committed by GitHub
parent 32935b04fa
commit 0dbc34ea56
19 changed files with 744 additions and 52 deletions

View File

@@ -0,0 +1,33 @@
---
title: "Cursor model listing"
version: 3.8.50
lastUpdated: 2026-08-09
---
# Cursor model listing
## Live catalog is exclusive when synced
After a successful Cursor model sync (`cursor-agent --list-models` → persisted
synced catalog), the **dashboard**, **`/v1/models`**, and **Test All** list:
1. Models returned by the live sync
2. Injected auto-router ids: `auto`, `auto-cost`, `auto-balance`, `auto-intelligence`
3. Operator **custom** models (Import / manual) — never pruned by sync
The large static registry under
`open-sse/config/providers/registry/cursor/` is **offline fallback only**. When
synced is empty (or discovery fails), listing falls back to that registry.
Effort-suffixed ids (for example `claude-4.6-sonnet-high`) may still be
**requested** at runtime: `resolveRequestedModel` strips the suffix into a wire
`ModelParameter`. Exclusive listing intentionally hides those static variants
from Test All so probes match what Cursor actually returns as available.
## Helpers
- `providerUsesExclusiveSyncedListing("cursor"|"cu")`
`src/lib/providers/modelListingCapability.ts`
- `mergeProviderModelListing` — dashboard merge
- `ensureCursorAutoCatalogEntry` — auto* inject on discovery + listing
- `shouldSuppressStaticModelForExclusiveListing``/v1/models` static loop

View File

@@ -1,5 +1,11 @@
{
"title": "Providers",
"description": "Provider-specific integration guides",
"pages": ["ALIBABA-QWEN-PROVIDER-FAMILIES", "CLAUDE_WEB", "AGENTROUTER", "ZED-DOCKER"]
"pages": [
"ALIBABA-QWEN-PROVIDER-FAMILIES",
"CLAUDE_WEB",
"AGENTROUTER",
"ZED-DOCKER",
"CURSOR-DOCKER"
]
}

View File

@@ -75,6 +75,7 @@ import {
visibleComposerContentFromThinking,
composerReasoningRemainder,
} from "./cursor/composer.ts";
import { getActiveSyncedCatalog } from "../../src/lib/db/models/activeSyncedCatalog.ts";
// Composer helpers re-exported for external importers (tests).
export {
isComposerModel,
@@ -805,6 +806,20 @@ export class CursorExecutor extends BaseExecutor {
return resolveCursorImages(imageUrls);
}
/**
* Exact ids from the active Cursor synced catalog. Empty/unavailable →
* undefined so resolveRequestedModel keeps #7289 offline splitting.
*/
private async loadLiveCatalogIds(): Promise<ReadonlySet<string> | undefined> {
try {
const catalog = await getActiveSyncedCatalog("cursor");
if (!catalog.models.length) return undefined;
return new Set(catalog.models.map((model) => model.id));
} catch {
return undefined;
}
}
private async buildRequest(
model: string,
body: {
@@ -819,7 +834,10 @@ export class CursorExecutor extends BaseExecutor {
}
): Promise<{ body: Uint8Array; blobStore: Map<string, Buffer> }> {
const { userText, tools } = this.assembleTextAndTools(body);
const images = await this.resolveRequestImages(body);
const [images, liveCatalogIds] = await Promise.all([
this.resolveRequestImages(body),
this.loadLiveCatalogIds(),
]);
const blobStore = new Map<string, Buffer>();
const requestBody = buildAgentRequestBody({
@@ -829,6 +847,7 @@ export class CursorExecutor extends BaseExecutor {
tools,
blobStore,
images,
liveCatalogIds,
});
return { body: requestBody, blobStore };
}

View File

@@ -332,6 +332,8 @@ function splitCursorEffortSuffix(
/**
* cursor-agent rewrites model ids before putting them on the wire:
* "auto" → RequestedModel { model_id: "default" }
* "auto-cost" → RequestedModel { model_id: "default",
* parameters: [{id: "optimization", value: "cost"}] }
* "composer-2-fast" → RequestedModel { model_id: "composer-2",
* parameters: [{id: "fast", value: "true"}] }
* "claude-opus-4-8-high" → RequestedModel { model_id: "claude-opus-4-8",
@@ -342,7 +344,31 @@ function splitCursorEffortSuffix(
* Other ids are passed through verbatim after spelling-variant normalization
* (see normalizeCursorModelId).
*/
export function resolveRequestedModel(modelId: string): {
/** Cursor Router optimization levels (OpenCodex `CURSOR_ROUTING_LEVELS`). */
export const CURSOR_ROUTING_LEVELS = ["cost", "balance", "intelligence"] as const;
export type CursorRoutingLevel = (typeof CURSOR_ROUTING_LEVELS)[number];
/**
* ModelParameter id for Cursor's Cost/Balance/Intelligence control on wire model
* `default` (OpenCodex `CURSOR_ROUTING_LEVEL_PARAMETER_ID`).
*/
export const CURSOR_ROUTING_LEVEL_PARAMETER_ID = "optimization";
export type ResolveRequestedModelOptions = {
/**
* When set and containing the normalized client model id, send that id
* verbatim on AgentRun (skip composer-fast / Claude / GPT splits).
* Live AvailableModels returns flattened effort-suffixed ids; stripping them
* to a missing base causes Cursor `AI Model Not Found`. Auto / auto-* still
* map to wire `default` (+ optimization) even when present in this set.
*/
liveCatalogIds?: ReadonlySet<string>;
};
export function resolveRequestedModel(
modelId: string,
opts?: ResolveRequestedModelOptions
): {
modelId: string;
parameters: Array<{ id: string; value: string }>;
} {
@@ -350,6 +376,20 @@ export function resolveRequestedModel(modelId: string): {
if (normalized === "auto") {
return { modelId: "default", parameters: [] };
}
// OpenCodex-style router variants: auto-cost / auto-balance / auto-intelligence
// → wire `default` + ModelParameter { id: "optimization", value: <level> }.
for (const level of CURSOR_ROUTING_LEVELS) {
if (normalized === `auto-${level}`) {
return {
modelId: "default",
parameters: [{ id: CURSOR_ROUTING_LEVEL_PARAMETER_ID, value: level }],
};
}
}
// Live catalog is authoritative for exact ids (flattened effort variants).
if (opts?.liveCatalogIds?.has(normalized)) {
return { modelId: normalized, parameters: [] };
}
// Strip the "-fast" suffix and surface it as a parameter — only the composer
// family observably needs this split today, but the protocol field is generic.
if (normalized.startsWith("composer-") && normalized.endsWith("-fast")) {
@@ -406,6 +446,8 @@ export type AgentRunInput = {
// encodeSelectedImageBody). Empty / undefined keeps the request
// byte-identical to the text-only path.
images?: EncodedImage[];
/** Exact live AvailableModels ids — see resolveRequestedModel liveCatalogIds. */
liveCatalogIds?: ReadonlySet<string>;
};
export { cursorImageAttachmentPath, encodeSelectedImageBody };
@@ -433,7 +475,9 @@ export function openAIToolsToMcpDefs(tools: OpenAITool[]): McpToolDefinition[] {
export function encodeAgentRunRequest(input: AgentRunInput): Buffer {
const conversationId = input.conversationId || crypto.randomUUID();
const messageId = input.messageId || crypto.randomUUID();
const { modelId, parameters } = resolveRequestedModel(input.modelId);
const { modelId, parameters } = resolveRequestedModel(input.modelId, {
liveCatalogIds: input.liveCatalogIds,
});
// UserMessage { text, message_id, selected_context, mode=1 }.
// selected_context is normally an empty placeholder (required by the server

View File

@@ -22,8 +22,11 @@ import {
getCompatibleFallbackModels,
} from "@/lib/providers/managedAvailableModels";
import { getProviderServiceKinds } from "@/lib/providers/serviceKindIndex";
import { providerLacksModelListing } from "@/lib/providers/modelListingCapability";
import { providerUsesCuratedModelsOnly } from "@/lib/providers/modelListingCapability";
import {
providerLacksModelListing,
providerUsesCuratedModelsOnly,
} from "@/lib/providers/modelListingCapability";
import { mergeProviderModelListing } from "@/lib/providers/mergeProviderModelListing";
import { normalizeModelCatalogSource } from "@/shared/utils/modelCatalogSearch";
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
@@ -270,39 +273,29 @@ export default function ProviderDetailPageClient() {
getProviderById(providerId)?.managedAccount === true;
const registryModels = getModelsByProviderId(providerId);
// Prefer synced API-discovered models when available, then merge built-ins
// and user-managed custom models without duplicating IDs.
// and user-managed custom models without duplicating IDs. Cursor exclusive
// listing drops the static registry entirely when synced is non-empty.
const models = useMemo(() => {
// Synced models keep their full property spread so provider-specific fields
// (e.g. Gemini's `supportedGenerationMethods`) survive into the table.
const builtInModels = registryModels.map((model) => ({
...model,
source: "system",
}));
const registryIds = new Set(builtInModels.map((m) => m.id));
const syncedExtras = (usesCuratedModelsOnly ? [] : syncedAvailableModels)
.filter((model: any) => model?.id && !registryIds.has(model.id))
.map((model: any) => ({
...model,
id: model.id,
name: model.name || model.id,
source: "imported",
}));
const knownIds = new Set([...registryIds, ...syncedExtras.map((model: any) => model.id)]);
const customExtras = (usesCuratedModelsOnly ? [] : modelMeta.customModels)
.filter((cm: any) => cm.id && !knownIds.has(cm.id))
.map((cm: any) => ({
id: cm.id,
name: cm.name || cm.id,
source: normalizeModelCatalogSource(cm.source) === "imported" ? "imported" : "custom",
}));
const allModels = [...builtInModels, ...syncedExtras, ...customExtras];
const deduped = new Map<string, (typeof allModels)[0]>();
for (const m of allModels) {
if (m.id && !deduped.has(m.id)) deduped.set(m.id, m);
}
return Array.from(deduped.values());
}, [registryModels, syncedAvailableModels, modelMeta.customModels, usesCuratedModelsOnly]);
return mergeProviderModelListing({
providerId,
registryModels,
syncedModels: syncedAvailableModels,
customModels: (modelMeta.customModels || []).map(
(cm: { id: string; name?: string; source?: string }) => ({
id: cm.id,
name: cm.name || cm.id,
source: normalizeModelCatalogSource(cm.source) === "imported" ? "imported" : "custom",
})
),
usesCuratedModelsOnly,
});
}, [
providerId,
registryModels,
syncedAvailableModels,
modelMeta.customModels,
usesCuratedModelsOnly,
]);
const isUpstreamProxyProvider = providerInfo?.category === "upstream-proxy";
const compatibleSupportsModelImport = compatibleProviderSupportsModelImport(providerId);

View File

@@ -92,6 +92,7 @@ import {
} from "@/lib/providerModels/geminiModelsParser";
import { getSyncedAvailableModels, getCustomModels } from "@/lib/db/models";
import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent";
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
import { fetchRaycastModels } from "@omniroute/open-sse/services/raycast.ts";
import { runWithProxyContext } from "@omniroute/open-sse/utils/proxyFetch.ts";
import {
@@ -1408,7 +1409,7 @@ export async function GET(
if (autoFetchDisabledResponse) return autoFetchDisabledResponse;
try {
const models = await fetchCursorAgentModels();
const models = ensureCursorAutoCatalogEntry(await fetchCursorAgentModels());
return buildApiDiscoveryResponse(models);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);

View File

@@ -14,7 +14,7 @@ import { createLazyConnectionView } from "@/lib/db/providers/lazyConnectionView"
import { extractAliasBackedModels } from "./aliasBackedModels";
import {
buildSyncedModelIdsByCanonicalProvider,
shouldSuppressStaticModelBySyncedCoverage,
shouldSuppressStaticModelForExclusiveListing,
} from "./catalogSyncedCoverage";
import { buildSyncedCapabilities, mergeSyncedCapabilities } from "./syncedCapabilities";
import { getAllEmbeddingModels } from "@omniroute/open-sse/config/embeddingRegistry";
@@ -41,7 +41,11 @@ import type { SyncedAvailableModel } from "@/lib/db/models";
import { getAllActiveSyncedModels } from "@/lib/db/models/activeSyncedCatalog";
import { getModelCatalogCacheVersion } from "@/lib/db/readCache";
import { getCompatibleFallbackModels } from "@/lib/providers/managedAvailableModels";
import { providerUsesCuratedModelsOnly } from "@/lib/providers/modelListingCapability";
import {
providerUsesCuratedModelsOnly,
providerUsesExclusiveSyncedListing,
} from "@/lib/providers/modelListingCapability";
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
import { getOpenRouterCatalog } from "@/lib/catalog/openrouterCatalog";
import { hasEligibleConnectionForModel } from "@/domain/connectionModelRules";
import {
@@ -737,14 +741,22 @@ async function buildUnifiedModelsResponseCore(
// `deepseek/deepseek-v4-flash` which its discovery never lists). Before
// the fix, a provider with any synced model silently dropped ALL its
// static models.
//
// Cursor exclusive listing: when an active synced catalog exists, drop
// ALL static rows (including effort variants) so Test All / clients only
// see live AvailableModels + injected auto*.
const syncedForProvider = syncedModelIdsByCanonicalProvider.get(canonicalProviderId);
const exclusiveListing = providerUsesExclusiveSyncedListing(canonicalProviderId);
const providerHasSynced = syncedForProvider !== undefined && syncedForProvider.size > 0;
const coveredBySynced = shouldSuppressStaticModelForExclusiveListing({
exclusiveListing,
providerHasSynced,
staticModelId: model.id,
syncedModelIds: syncedForProvider ? [...syncedForProvider] : [],
});
if (
shouldSuppressStaticModelBySyncedCoverage({
providerHasSynced: syncedForProvider !== undefined && syncedForProvider.size > 0,
staticModelId: model.id,
syncedModelIds: syncedForProvider ? [...syncedForProvider] : [],
}) &&
!isRegisteredEffortVariant(providerModels, model.id)
coveredBySynced &&
(exclusiveListing || !isRegisteredEffortVariant(providerModels, model.id))
)
continue;
if (!providerSupportsModel(canonicalProviderId, model.id)) continue;
@@ -838,7 +850,16 @@ async function buildUnifiedModelsResponseCore(
continue;
}
for (const sm of syncedModels) {
for (const sm of providerUsesExclusiveSyncedListing(providerId)
? ensureCursorAutoCatalogEntry(
syncedModels.map((row) => ({
...row,
id: row.id,
name: row.name || row.id,
owned_by: "cursor",
}))
)
: syncedModels) {
if (!providerSupportsModel(canonicalProviderId, sm.id)) continue;
if (canonicalProviderId === "codex" && isCodexDiscoveryModelExcluded(sm)) {
continue;
@@ -1073,9 +1094,7 @@ async function buildUnifiedModelsResponseCore(
// here would discard all but the last segment and miss stored flags for
// providers whose model IDs carry a sub-path (e.g. OpenRouter scoped models).
const getSpecialtyModelRelativeId = (modelId: string, provider: string): string =>
modelId.startsWith(`${provider}/`)
? modelId.slice(provider.length + 1)
: modelId;
modelId.startsWith(`${provider}/`) ? modelId.slice(provider.length + 1) : modelId;
// Add embedding models (filtered by active providers)
for (const embModel of getAllEmbeddingModels()) {

View File

@@ -33,6 +33,29 @@ export function shouldSuppressStaticModelBySyncedCoverage(opts: {
return opts.syncedModelIds.includes(opts.staticModelId);
}
/**
* Exclusive live-catalog listing (Cursor): when the provider opts in and has a
* non-empty synced catalog, suppress EVERY static registry row — including
* effort-suffixed variants the coverage helper would otherwise preserve.
*
* Non-exclusive providers fall through to exact-id coverage suppression.
*/
export function shouldSuppressStaticModelForExclusiveListing(opts: {
exclusiveListing: boolean;
providerHasSynced: boolean;
staticModelId: string;
syncedModelIds: string[];
}): boolean {
if (opts.exclusiveListing) {
return opts.providerHasSynced && opts.syncedModelIds.length > 0;
}
return shouldSuppressStaticModelBySyncedCoverage({
providerHasSynced: opts.providerHasSynced,
staticModelId: opts.staticModelId,
syncedModelIds: opts.syncedModelIds,
});
}
/**
* Build a Map of canonical provider id -> set of synced display-model ids, so the
* static loop can decide which static models a provider's synced discovery list

View File

@@ -204,8 +204,18 @@ export async function reconcileProvidersWithActiveSyncedCatalog(
for (const { provider, catalog } of states) {
const modelIsLive = catalog.models.some((model) => model.id === modelId);
// Cursor auto-router: always allow `auto` / router variants even if a stale live
// catalog omitted them (AvailableModels / agent list often returns wire id
// `default` only; listing injects `auto` + cost/balance/intelligence).
const cursorAutoAllow =
provider === "cursor" &&
(modelId === "auto" ||
modelId === "default" ||
modelId === "auto-cost" ||
modelId === "auto-balance" ||
modelId === "auto-intelligence");
if (!catalog.authoritative || modelIsLive) {
if (!catalog.authoritative || modelIsLive || cursorAutoAllow) {
providers.push(provider);
} else {
excludedProviders.push(provider);

View File

@@ -0,0 +1,60 @@
/**
* Ensure Cursor catalog listings always expose OmniRoute's public auto-router
* ids (`auto` + Cost/Balance/Intelligence variants). Live AvailableModels /
* cursor-agent often return wire id `default` only.
*/
export type CursorAutoCatalogEntry = {
id: string;
name: string;
owned_by?: string;
[key: string]: unknown;
};
export const CURSOR_AUTO_ROUTER_VARIANT_IDS = [
"auto-cost",
"auto-balance",
"auto-intelligence",
] as const;
const CURSOR_AUTO_ROUTER_VARIANT_NAMES: Record<
(typeof CURSOR_AUTO_ROUTER_VARIANT_IDS)[number],
string
> = {
"auto-cost": "Auto (cost)",
"auto-balance": "Auto (balance)",
"auto-intelligence": "Auto (intelligence)",
};
/** Cursor auto-router: catalog id `auto`, wire id `default`. Always keep `auto` visible. */
export function ensureCursorAutoCatalogEntry<T extends CursorAutoCatalogEntry>(models: T[]): T[] {
const byId = new Map(models.map((m) => [m.id, m]));
const out = [...models];
if (!byId.has("auto")) {
const defaultEntry = byId.get("default");
const autoEntry = {
id: "auto",
name:
typeof defaultEntry?.name === "string" && defaultEntry.name.trim()
? defaultEntry.name
: "Auto (current, default)",
owned_by: "cursor",
} as T;
out.unshift(autoEntry);
byId.set("auto", autoEntry);
}
for (const id of CURSOR_AUTO_ROUTER_VARIANT_IDS) {
if (byId.has(id)) continue;
const entry = {
id,
name: CURSOR_AUTO_ROUTER_VARIANT_NAMES[id],
owned_by: "cursor",
} as T;
out.push(entry);
byId.set(id, entry);
}
return out;
}

View File

@@ -0,0 +1,96 @@
/**
* Pure merge of registry / synced / custom model rows for the provider detail
* dashboard (and thus Test All targets). Cursor exclusive listing prefers the
* live synced catalog when non-empty.
*/
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
import {
providerUsesCuratedModelsOnly,
providerUsesExclusiveSyncedListing,
} from "@/lib/providers/modelListingCapability";
export type ProviderListingModel = {
id: string;
name?: string;
source?: string;
[key: string]: unknown;
};
export type MergeProviderModelListingInput = {
providerId: string;
registryModels: Array<{ id: string; name?: string; [key: string]: unknown }>;
syncedModels: Array<{ id: string; name?: string; [key: string]: unknown }>;
customModels: Array<{ id: string; name?: string; source?: string; [key: string]: unknown }>;
usesCuratedModelsOnly?: boolean;
};
function normalizeCustomSource(source: unknown): "imported" | "custom" {
return source === "imported" ? "imported" : "custom";
}
function dedupeById(models: ProviderListingModel[]): ProviderListingModel[] {
const deduped = new Map<string, ProviderListingModel>();
for (const m of models) {
if (m.id && !deduped.has(m.id)) deduped.set(m.id, m);
}
return Array.from(deduped.values());
}
export function mergeProviderModelListing(
input: MergeProviderModelListingInput
): ProviderListingModel[] {
const curated =
input.usesCuratedModelsOnly === true || providerUsesCuratedModelsOnly(input.providerId);
const synced = curated ? [] : input.syncedModels.filter((m) => m?.id);
const custom = curated ? [] : input.customModels.filter((m) => m?.id);
const exclusive = providerUsesExclusiveSyncedListing(input.providerId) && synced.length > 0;
if (exclusive) {
const withAuto = ensureCursorAutoCatalogEntry(
synced.map((model) => ({
...model,
id: model.id,
name: model.name || model.id,
owned_by: "cursor",
source: "imported",
}))
);
const knownIds = new Set(withAuto.map((m) => m.id));
const customExtras = custom
.filter((cm) => cm.id && !knownIds.has(cm.id))
.map((cm) => ({
...cm,
id: cm.id,
name: cm.name || cm.id,
source: normalizeCustomSource(cm.source),
}));
return dedupeById([...withAuto, ...customExtras]);
}
const builtInModels = input.registryModels.map((model) => ({
...model,
source: "system",
}));
const registryIds = new Set(builtInModels.map((m) => m.id));
const syncedExtras = synced
.filter((model) => model.id && !registryIds.has(model.id))
.map((model) => ({
...model,
id: model.id,
name: model.name || model.id,
source: "imported",
}));
const knownIds = new Set([...registryIds, ...syncedExtras.map((m) => m.id)]);
const customExtras = custom
.filter((cm) => cm.id && !knownIds.has(cm.id))
.map((cm) => ({
...cm,
id: cm.id,
name: cm.name || cm.id,
source: normalizeCustomSource(cm.source),
}));
return dedupeById([...builtInModels, ...syncedExtras, ...customExtras]);
}

View File

@@ -17,6 +17,19 @@ export function providerUsesCuratedModelsOnly(providerId: string): boolean {
return CURATED_MODEL_ONLY_PROVIDERS.has(providerId.trim().toLowerCase());
}
/**
* Providers whose non-empty synced AvailableModels catalog fully replaces the
* static registry for dashboard / `/v1/models` / Test All listing. Static rows
* remain offline fallback only when synced is empty.
*
* Cursor-only for now — other authoritative live-catalog providers keep
* coverage-style static preservation (e.g. command-code uncovered static ids).
*/
export function providerUsesExclusiveSyncedListing(providerId: string): boolean {
const id = providerId.trim().toLowerCase();
return id === "cursor" || id === "cu";
}
/**
* True when the provider is tool-only and therefore has no model listing:
* - its id ends in `-search` (legacy search providers), OR

View File

@@ -4,6 +4,7 @@ import assert from "node:assert/strict";
import {
buildSyncedModelIdsByCanonicalProvider,
shouldSuppressStaticModelBySyncedCoverage,
shouldSuppressStaticModelForExclusiveListing,
} from "../../src/app/api/v1/models/catalogSyncedCoverage.ts";
import type { SyncedAvailableModel } from "../../src/lib/db/models/synced.ts";
@@ -75,3 +76,57 @@ test("buildSyncedModelIdsByCanonicalProvider groups synced ids by canonical prov
assert.ok(ds);
assert.ok(ds.has("deepseek-v4-flash"));
});
test("exclusive listing: any static row suppressed when provider has synced catalog", () => {
assert.equal(
shouldSuppressStaticModelForExclusiveListing({
exclusiveListing: true,
providerHasSynced: true,
staticModelId: "claude-4.6-sonnet-high",
syncedModelIds: ["claude-4.6-sonnet", "composer-2.5"],
}),
true
);
assert.equal(
shouldSuppressStaticModelForExclusiveListing({
exclusiveListing: true,
providerHasSynced: true,
staticModelId: "composer-2.5",
syncedModelIds: ["claude-4.6-sonnet", "composer-2.5"],
}),
true
);
});
test("exclusive listing: does not suppress when synced is empty", () => {
assert.equal(
shouldSuppressStaticModelForExclusiveListing({
exclusiveListing: true,
providerHasSynced: false,
staticModelId: "claude-4.6-sonnet-high",
syncedModelIds: [],
}),
false
);
});
test("exclusive listing: non-exclusive providers keep coverage behavior", () => {
assert.equal(
shouldSuppressStaticModelForExclusiveListing({
exclusiveListing: false,
providerHasSynced: true,
staticModelId: "deepseek/deepseek-v4-flash",
syncedModelIds: ["gpt-5.6-luna"],
}),
false
);
assert.equal(
shouldSuppressStaticModelForExclusiveListing({
exclusiveListing: false,
providerHasSynced: true,
staticModelId: "gpt-5.6-luna",
syncedModelIds: ["gpt-5.6-luna"],
}),
true
);
});

View File

@@ -31,6 +31,18 @@ import {
test("resolveRequestedModel maps cursor-agent's client-side aliases", () => {
assert.deepEqual(resolveRequestedModel("auto"), { modelId: "default", parameters: [] });
assert.deepEqual(resolveRequestedModel("auto-cost"), {
modelId: "default",
parameters: [{ id: "optimization", value: "cost" }],
});
assert.deepEqual(resolveRequestedModel("auto-balance"), {
modelId: "default",
parameters: [{ id: "optimization", value: "balance" }],
});
assert.deepEqual(resolveRequestedModel("auto-intelligence"), {
modelId: "default",
parameters: [{ id: "optimization", value: "intelligence" }],
});
assert.deepEqual(resolveRequestedModel("composer-2-fast"), {
modelId: "composer-2",
parameters: [{ id: "fast", value: "true" }],

View File

@@ -0,0 +1,31 @@
import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { ensureCursorAutoCatalogEntry } from "@/lib/providerModels/cursorAutoCatalog";
describe("ensureCursorAutoCatalogEntry", () => {
it("injects auto when only wire id default is present", () => {
const models = ensureCursorAutoCatalogEntry([
{ id: "default", name: "Auto", owned_by: "cursor" },
{ id: "composer-2.5", name: "Composer 2.5", owned_by: "cursor" },
]);
assert.ok(models.some((m) => m.id === "auto"));
assert.ok(models.some((m) => m.id === "default"));
});
it("injects auto-cost / auto-balance / auto-intelligence", () => {
const models = ensureCursorAutoCatalogEntry([{ id: "auto", name: "Auto", owned_by: "cursor" }]);
const ids = models.map((m) => m.id);
assert.ok(ids.includes("auto-cost"));
assert.ok(ids.includes("auto-balance"));
assert.ok(ids.includes("auto-intelligence"));
});
it("does not duplicate existing auto entries", () => {
const models = ensureCursorAutoCatalogEntry([
{ id: "auto", name: "Auto", owned_by: "cursor" },
{ id: "auto-cost", name: "Auto (cost)", owned_by: "cursor" },
]);
assert.equal(models.filter((m) => m.id === "auto").length, 1);
assert.equal(models.filter((m) => m.id === "auto-cost").length, 1);
});
});

View File

@@ -0,0 +1,79 @@
/**
* Cursor exclusive live-catalog listing: when synced AvailableModels exists,
* dashboard / Test All must list synced + injected auto* + custom only —
* never the static registry effort/premium rows.
*/
import { strict as assert } from "node:assert";
import { describe, it } from "node:test";
import { mergeProviderModelListing } from "@/lib/providers/mergeProviderModelListing";
describe("mergeProviderModelListing (cursor exclusive)", () => {
const registry = [
{ id: "auto", name: "Auto (Server Picks)" },
{ id: "claude-4.6-sonnet-high", name: "Claude Sonnet High" },
{ id: "gpt-5.5-high", name: "GPT 5.5 High" },
{ id: "composer-2.5", name: "Composer 2.5" },
];
it("exclusive + synced: drops static-only ids and injects auto*", () => {
const models = mergeProviderModelListing({
providerId: "cursor",
registryModels: registry,
syncedModels: [
{ id: "composer-2.5", name: "Composer 2.5" },
{ id: "claude-4.6-sonnet", name: "Claude 4.6 Sonnet" },
],
customModels: [],
usesCuratedModelsOnly: false,
});
const ids = models.map((m) => m.id);
assert.ok(ids.includes("composer-2.5"));
assert.ok(ids.includes("claude-4.6-sonnet"));
assert.ok(ids.includes("auto"));
assert.ok(ids.includes("auto-cost"));
assert.ok(ids.includes("auto-balance"));
assert.ok(ids.includes("auto-intelligence"));
assert.equal(ids.includes("claude-4.6-sonnet-high"), false);
assert.equal(ids.includes("gpt-5.5-high"), false);
assert.ok(models.every((m) => m.source === "imported" || m.id.startsWith("auto")));
});
it("exclusive + synced: keeps operator custom models", () => {
const models = mergeProviderModelListing({
providerId: "cursor",
registryModels: registry,
syncedModels: [{ id: "composer-2.5", name: "Composer 2.5" }],
customModels: [{ id: "my-custom", name: "My Custom", source: "custom" }],
usesCuratedModelsOnly: false,
});
const custom = models.find((m) => m.id === "my-custom");
assert.ok(custom);
assert.equal(custom.source, "custom");
});
it("exclusive + empty synced: falls back to registry custom", () => {
const models = mergeProviderModelListing({
providerId: "cursor",
registryModels: registry,
syncedModels: [],
customModels: [{ id: "my-custom", name: "My Custom" }],
usesCuratedModelsOnly: false,
});
const ids = models.map((m) => m.id);
assert.ok(ids.includes("claude-4.6-sonnet-high"));
assert.ok(ids.includes("my-custom"));
});
it("non-exclusive providers keep registry-first merge", () => {
const models = mergeProviderModelListing({
providerId: "openai",
registryModels: [{ id: "gpt-4o", name: "GPT-4o" }],
syncedModels: [{ id: "gpt-4o-mini", name: "GPT-4o mini" }],
customModels: [],
usesCuratedModelsOnly: false,
});
const ids = models.map((m) => m.id);
assert.deepEqual(ids, ["gpt-4o", "gpt-4o-mini"]);
});
});

View File

@@ -0,0 +1,90 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
resolveRequestedModel,
encodeAgentRunRequest,
} from "../../open-sse/utils/cursorAgentProtobuf";
import { CURSOR_REWRITE_FAILURE_IDS } from "./fixtures/cursor-rewrite-failure-ids";
test("resolveRequestedModel passes live-catalog Claude effort ids through verbatim", () => {
const live = new Set(["claude-opus-5-low"]);
assert.deepEqual(resolveRequestedModel("claude-opus-5-low", { liveCatalogIds: live }), {
modelId: "claude-opus-5-low",
parameters: [],
});
});
test("resolveRequestedModel passes live-catalog GPT reasoning ids through verbatim", () => {
const live = new Set(["gpt-5.6-sol-medium"]);
assert.deepEqual(resolveRequestedModel("gpt-5.6-sol-medium", { liveCatalogIds: live }), {
modelId: "gpt-5.6-sol-medium",
parameters: [],
});
});
test("resolveRequestedModel still strips effort when id is absent from live catalog", () => {
const live = new Set(["composer-2"]);
assert.deepEqual(resolveRequestedModel("claude-opus-5-low", { liveCatalogIds: live }), {
modelId: "claude-opus-5",
parameters: [{ id: "effort", value: "low" }],
});
assert.deepEqual(resolveRequestedModel("gpt-5.5-high", { liveCatalogIds: live }), {
modelId: "gpt-5.5",
parameters: [{ id: "reasoning", value: "high" }],
});
});
test("resolveRequestedModel still strips effort when liveCatalogIds is omitted", () => {
assert.deepEqual(resolveRequestedModel("claude-opus-4-8-high"), {
modelId: "claude-opus-4-8",
parameters: [{ id: "effort", value: "high" }],
});
});
test("resolveRequestedModel maps auto to default even when auto is in the live catalog", () => {
const live = new Set(["auto", "auto-cost", "claude-opus-5-low"]);
assert.deepEqual(resolveRequestedModel("auto", { liveCatalogIds: live }), {
modelId: "default",
parameters: [],
});
assert.deepEqual(resolveRequestedModel("auto-cost", { liveCatalogIds: live }), {
modelId: "default",
parameters: [{ id: "optimization", value: "cost" }],
});
});
test("resolveRequestedModel passes composer-*-fast through when present in live catalog", () => {
const live = new Set(["composer-2.5-fast", "composer-2-fast"]);
assert.deepEqual(resolveRequestedModel("composer-2.5-fast", { liveCatalogIds: live }), {
modelId: "composer-2.5-fast",
parameters: [],
});
assert.deepEqual(resolveRequestedModel("composer-2-fast", { liveCatalogIds: live }), {
modelId: "composer-2-fast",
parameters: [],
});
});
test("resolveRequestedModel passes every rewrite-failure id through when all are live", () => {
const live = new Set<string>(CURSOR_REWRITE_FAILURE_IDS);
for (const id of CURSOR_REWRITE_FAILURE_IDS) {
assert.deepEqual(
resolveRequestedModel(id, { liveCatalogIds: live }),
{ modelId: id, parameters: [] },
id
);
}
});
test("encodeAgentRunRequest embeds verbatim live catalog model id", () => {
const live = new Set(["claude-opus-5-low"]);
const buf = encodeAgentRunRequest({
modelId: "claude-opus-5-low",
userText: "hi",
liveCatalogIds: live,
});
const text = buf.toString("latin1");
const full = text.split("claude-opus-5-low").length - 1;
assert.ok(full >= 4, `verbatim id must appear in RequestedModel + ModelDetails (got ${full})`);
assert.ok(!text.includes("effort"), "must not emit effort parameter for live verbatim id");
});

View File

@@ -0,0 +1,92 @@
/**
* Live-synced Cursor model ids that Test All failed when #7289
* resolveRequestedModel stripped them to a missing base + parameter.
* Smoke checklist for catalog-aware pass-through.
*/
export const CURSOR_REWRITE_FAILURE_IDS = [
// Claude (52)
"claude-4.5-opus-high",
"claude-4.6-opus-high",
"claude-4.6-opus-max",
"claude-4.6-sonnet-medium",
"claude-fable-5-low",
"claude-fable-5-medium",
"claude-fable-5-high",
"claude-fable-5-xhigh",
"claude-fable-5-max",
"claude-fable-5-thinking-low",
"claude-fable-5-thinking-medium",
"claude-fable-5-thinking-high",
"claude-fable-5-thinking-xhigh",
"claude-fable-5-thinking-max",
"claude-opus-4-7-low",
"claude-opus-4-7-medium",
"claude-opus-4-7-high",
"claude-opus-4-7-xhigh",
"claude-opus-4-7-max",
"claude-opus-4-7-thinking-low",
"claude-opus-4-7-thinking-medium",
"claude-opus-4-7-thinking-high",
"claude-opus-4-7-thinking-xhigh",
"claude-opus-4-7-thinking-max",
"claude-opus-4-8-low",
"claude-opus-4-8-medium",
"claude-opus-4-8-high",
"claude-opus-4-8-xhigh",
"claude-opus-4-8-max",
"claude-opus-4-8-thinking-low",
"claude-opus-4-8-thinking-medium",
"claude-opus-4-8-thinking-high",
"claude-opus-4-8-thinking-xhigh",
"claude-opus-4-8-thinking-max",
"claude-opus-5-low",
"claude-opus-5-medium",
"claude-opus-5-high",
"claude-opus-5-thinking-low",
"claude-opus-5-thinking-medium",
"claude-opus-5-thinking-high",
"claude-opus-5-thinking-xhigh",
"claude-opus-5-thinking-max",
"claude-sonnet-5-low",
"claude-sonnet-5-medium",
"claude-sonnet-5-high",
"claude-sonnet-5-xhigh",
"claude-sonnet-5-max",
"claude-sonnet-5-thinking-low",
"claude-sonnet-5-thinking-medium",
"claude-sonnet-5-thinking-high",
"claude-sonnet-5-thinking-xhigh",
"claude-sonnet-5-thinking-max",
// GPT (31)
"gpt-5.4-low",
"gpt-5.4-medium",
"gpt-5.4-high",
"gpt-5.4-xhigh",
"gpt-5.4-mini-low",
"gpt-5.4-mini-medium",
"gpt-5.4-mini-high",
"gpt-5.4-mini-xhigh",
"gpt-5.4-nano-low",
"gpt-5.4-nano-medium",
"gpt-5.4-nano-high",
"gpt-5.4-nano-xhigh",
"gpt-5.5-low",
"gpt-5.5-medium",
"gpt-5.5-high",
"gpt-5.5-extra-high",
"gpt-5.6-sol-low",
"gpt-5.6-sol-medium",
"gpt-5.6-sol-high",
"gpt-5.6-sol-xhigh",
"gpt-5.6-sol-max",
"gpt-5.6-terra-low",
"gpt-5.6-terra-medium",
"gpt-5.6-terra-high",
"gpt-5.6-terra-xhigh",
"gpt-5.6-terra-max",
"gpt-5.6-luna-low",
"gpt-5.6-luna-medium",
"gpt-5.6-luna-high",
"gpt-5.6-luna-xhigh",
"gpt-5.6-luna-max",
] as const;

View File

@@ -6,6 +6,7 @@ import { describe, it } from "node:test";
import {
providerLacksModelListing,
providerUsesCuratedModelsOnly,
providerUsesExclusiveSyncedListing,
} from "@/lib/providers/modelListingCapability";
describe("providerLacksModelListing (#5420)", () => {
@@ -36,3 +37,18 @@ describe("providerLacksModelListing (#5420)", () => {
assert.equal(providerUsesCuratedModelsOnly("kimi-coding"), false);
});
});
describe("providerUsesExclusiveSyncedListing", () => {
it("is true only for Cursor (id or alias)", () => {
assert.equal(providerUsesExclusiveSyncedListing("cursor"), true);
assert.equal(providerUsesExclusiveSyncedListing("cu"), true);
assert.equal(providerUsesExclusiveSyncedListing("Cursor"), true);
});
it("is false for other providers including authoritative live-catalog ones", () => {
assert.equal(providerUsesExclusiveSyncedListing("github"), false);
assert.equal(providerUsesExclusiveSyncedListing("command-code"), false);
assert.equal(providerUsesExclusiveSyncedListing("openai"), false);
assert.equal(providerUsesExclusiveSyncedListing(""), false);
});
});