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
16 changes: 13 additions & 3 deletions src/model-variants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,19 @@ export function buildModelVariants(item: ModelListItem): Record<string, CursorVa
}

for (const value of values) {
// Key by the bare value (e.g. "high"); prefix with the param id only
// when two params share a value (e.g. reasoning-low vs effort-low).
const key = out[value] === undefined ? value : `${param.id}-${value}`;
// `none` means reasoning OFF — the model's default when no variant is
// selected. Surfacing it as a selectable variant is meaningless (you
// get it by picking nothing), so skip it. Standard providers
// (models.dev) include `none` in their effort values, but the
// no-variant-selected state already represents it.
if (value === "none") continue;
// Cursor labels the top reasoning tier "extra-high"; the opencode
// standard (models.dev) calls it "xhigh". Use the standard label for
// the variant key so the cycler is consistent across providers, but
// keep the Cursor wire-format value ("extra-high") in the params sent
// to the API.
const displayKey = value === "extra-high" ? "xhigh" : value;
const key = out[displayKey] === undefined ? displayKey : `${param.id}-${displayKey}`;
out[key] = { params: { ...defaults, [param.id]: value } };
}
continue;
Expand Down
73 changes: 73 additions & 0 deletions test/model-variants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,79 @@ describe("buildModelVariants", () => {
});
});

it("renames extra-high to xhigh in the variant key but keeps the wire value", () => {
// Cursor labels the top reasoning tier "extra-high"; the opencode standard
// (models.dev) calls it "xhigh". The variant KEY normalizes to xhigh so
// the cycler is consistent across providers; the param VALUE sent to
// Cursor's API stays "extra-high".
const variants = buildModelVariants(
model([{ id: "reasoning", values: [{ value: "low" }, { value: "extra-high" }] }]),
);
expect(variants).toEqual({
low: { params: { reasoning: "low" } },
xhigh: { params: { reasoning: "extra-high" } },
});
});

it("drops the 'none' reasoning value (it is the model default)", () => {
// `none` = reasoning OFF, which is what you get by selecting no variant.
// Surfacing it as a selectable entry is meaningless, so it is skipped.
const variants = buildModelVariants(
model([
{ id: "reasoning", values: [{ value: "none" }, { value: "low" }, { value: "high" }] },
]),
);
expect(variants).toEqual({
low: { params: { reasoning: "low" } },
high: { params: { reasoning: "high" } },
});
});

it("composes none-drop and extra-high rename for the real GPT shape", () => {
// gpt-5.5 / gpt-5.4 catalog: reasoning=[none,low,medium,high,extra-high]
// + fast. Expect: low, medium, high, xhigh (no none, extra-high→xhigh),
// each effort variant bakes fast OFF, plus a standalone fast opt-in.
const variants = buildModelVariants(
model([
{
id: "reasoning",
values: [
{ value: "none" },
{ value: "low" },
{ value: "medium" },
{ value: "high" },
{ value: "extra-high" },
],
},
{ id: "fast", values: [{ value: "false" }, { value: "true" }] },
]),
);
expect(variants).toEqual({
low: { params: { reasoning: "low", fast: "false" } },
medium: { params: { reasoning: "medium", fast: "false" } },
high: { params: { reasoning: "high", fast: "false" } },
xhigh: { params: { reasoning: "extra-high", fast: "false" } },
fast: { params: { fast: "true" } },
});
});

it("prefixes the display key on a collision involving extra-high", () => {
// Defensive: if two reasoning params both resolve to the xhigh display key
// (one via the real "xhigh" value, one via "extra-high"→xhigh), the second
// is prefixed with its param id. No current catalog model hits this, but
// the guard must hold.
const variants = buildModelVariants(
model([
{ id: "effort", values: [{ value: "xhigh" }] },
{ id: "reasoning", values: [{ value: "extra-high" }] },
]),
);
expect(variants).toEqual({
xhigh: { params: { effort: "xhigh" } },
"reasoning-xhigh": { params: { reasoning: "extra-high" } },
});
});

it("surfaces a non-reasoning boolean (fast) as an opt-in toggle; ignores enum context", () => {
// `fast` is Cursor's fast-tier toggle. It is not a reasoning level, but the
// user must be able to opt INTO it from the picker (default is off, sent
Expand Down