Skip to content
Merged
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
19 changes: 16 additions & 3 deletions src/adapters/cursor/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,27 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
...CURSOR_REAL_ID_EXCEPTIONS,
]);

/** Picker labels for every seeded row, for providers.cursor.modelDisplayNames. */
/**
* Picker labels for providers.cursor.modelDisplayNames.
*
* Only labels that carry Cursor's own product name ("Cursor Grok 4.6") are published. Every
* other row keeps the routed `cursor/<id>` slug that the rest of the picker uses, so a Cursor
* row reads like its siblings from other providers instead of an unprefixed marketing name.
* #3222 labeled every row and that dropped the `cursor/` prefix from the picker.
*/
export function cursorModelDisplayNames(): Record<string, string> {
return Object.fromEntries([
const labels: (readonly [string, string])[] = [
...CURSOR_ROUTER_MODEL_IDS.map(id => [id, CURSOR_ROUTER_DISPLAY_NAMES[id] ?? id] as const),
...cursorUmbrellaRows().map(row => [row.id, row.displayName] as const),
...CURSOR_PRODUCT_MODELS.map(model => [model.id, model.displayName] as const),
...CURSOR_REAL_ID_EXCEPTIONS.map(model => [model.id, model.displayName] as const),
]);
];
return Object.fromEntries(labels.filter(([, label]) => isCursorBrandedLabel(label)));
}

/** A label Cursor itself brands with its name, e.g. "Cursor Grok 4.6". */
export function isCursorBrandedLabel(label: string): boolean {
return /^cursor\b/i.test(label.trim());
}

export function cursorModelIds(models: readonly CursorModelInfo[] = CURSOR_STATIC_MODELS): string[] {
Expand Down
46 changes: 29 additions & 17 deletions tests/cursor-display-names.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { describe, expect, test } from "bun:test";
import { cursorModelDisplayNames } from "../src/adapters/cursor/discovery";
import { cursorModelDisplayNames, CURSOR_STATIC_MODELS, isCursorBrandedLabel } from "../src/adapters/cursor/discovery";
import { cursorUmbrellaRows } from "../src/adapters/cursor/catalog";
import { enrichProviderFromRegistry, providerConfigSeed } from "../src/providers/derive";
import { getProviderRegistryEntry } from "../src/providers/registry";
import { configuredModelDisplayName } from "../src/codex/catalog/provider-fetch";
import type { OcxProviderConfig } from "../src/types";

/**
* The Codex picker showed raw slugs (`cursor/kimi-k3`) because `routedDisplayName`
* (codex/catalog/sync.ts) passes a routed slug through unchanged, and nothing carried
* Cursor's labels into `providers.cursor.modelDisplayNames` — the registry entry type had
* no such field. These assert the full registry -> config -> catalog-hint path, not just
* that a label table exists (devlog 260902_cursor_unified_identity).
* `routedDisplayName` (codex/catalog/sync.ts) passes a routed slug through unchanged, so a
* Cursor row reads `cursor/kimi-k3` like every other provider's rows. #3222 labeled every
* seeded row and the picker lost its `cursor/` prefix, which made Cursor rows
* indistinguishable from the same model under another provider. Only labels that carry
* Cursor's own brand ("Cursor Grok 4.6") are published; the rest keep the routed slug.
* These assert the full registry -> config -> catalog-hint path, not just that a label
* table exists.
*/
describe("cursor picker labels reach the catalog", () => {
const cursorEntry = () => {
Expand All @@ -20,24 +22,33 @@ describe("cursor picker labels reach the catalog", () => {
return entry;
};

test("the registry entry carries a label for every seeded row", () => {
test("the registry entry labels only Cursor-branded rows", () => {
const labels = cursorModelDisplayNames();
expect(cursorEntry().modelDisplayNames).toEqual(labels);
const seededIds = new Set(CURSOR_STATIC_MODELS.map(model => model.id));
for (const [id, label] of Object.entries(labels)) {
expect(seededIds.has(id)).toBe(true);
expect(isCursorBrandedLabel(label)).toBe(true);
}
for (const row of cursorUmbrellaRows()) {
expect(labels[row.id]).toBe(row.displayName);
if (isCursorBrandedLabel(row.displayName)) expect(labels[row.id]).toBe(row.displayName);
else expect(labels).not.toHaveProperty(row.id);
}
// The label is a human name, never the id echoed back.
expect(labels["kimi-k3"]).toBe("Kimi K3");
// Cursor's own product name stays; a third-party model keeps its `cursor/<id>` slug.
expect(labels["grok-4.6"]).toBe("Cursor Grok 4.6");
expect(labels["claude-opus-5"]).toBe("Claude Opus 5");
expect(labels.auto).toBe("Auto");
expect(labels["grok-4.5"]).toBe("Cursor Grok 4.5");
expect(labels).not.toHaveProperty("kimi-k3");
expect(labels).not.toHaveProperty("claude-opus-5");
expect(labels).not.toHaveProperty("auto");
expect(labels).not.toHaveProperty("composer-2.5");
});

test("a fresh seed exposes the labels through configuredModelDisplayName", () => {
test("a fresh seed exposes only the branded labels through configuredModelDisplayName", () => {
const seeded = providerConfigSeed(cursorEntry());
expect(configuredModelDisplayName(seeded, "kimi-k3")).toBe("Kimi K3");
expect(configuredModelDisplayName(seeded, "claude-4-sonnet-1m")).toBe("Claude Sonnet 4 (1M)");
expect(configuredModelDisplayName(seeded, "composer-2.5-fast")).toBe("Composer 2.5 Fast");
expect(configuredModelDisplayName(seeded, "grok-4.6")).toBe("Cursor Grok 4.6");
expect(configuredModelDisplayName(seeded, "kimi-k3")).toBeUndefined();
expect(configuredModelDisplayName(seeded, "claude-4-sonnet-1m")).toBeUndefined();
expect(configuredModelDisplayName(seeded, "composer-2.5-fast")).toBeUndefined();
});

test("enrich backfills an existing install per model, preserving operator renames", () => {
Expand All @@ -49,7 +60,8 @@ describe("cursor picker labels reach the catalog", () => {
enrichProviderFromRegistry("cursor", existing);
// Operator value survives...
expect(configuredModelDisplayName(existing, "kimi-k3")).toBe("My K3");
// ...while every other row still gains its label instead of staying unlabeled.
// ...the branded row gains its label, and an unbranded row stays on its routed slug.
expect(configuredModelDisplayName(existing, "grok-4.6")).toBe("Cursor Grok 4.6");
expect(configuredModelDisplayName(existing, "claude-opus-5")).toBeUndefined();
});
});
Loading