Skip to content

Commit e72cab7

Browse files
committed
fix(variants): normalize enum keys and drop 'none' for provider parity
Cursor GPT models expose a 'none' reasoning value (reasoning OFF) and label the top tier 'extra-high'. Standard opencode providers (models.dev) use 'xhigh' for that tier and treat 'none' as the implicit default, not a selectable level. The cycler therefore showed 'none' and 'extra-high' as variant keys, breaking parity with how the same models appear under other providers. Skip 'none' in the enum reasoning branch — it's the model default (no variant selected). Alias 'extra-high' to 'xhigh' for the variant KEY so the picker label matches the standard; the param VALUE sent to Cursor's API stays 'extra-high' (wire format unchanged). The collision-prefix guard now uses the display key. Tests: extra-high→xhigh rename, none dropped, realistic GPT shape (none+extra-high+fast composition), and an extra-high/xhigh collision guard.
1 parent b2a81b7 commit e72cab7

2 files changed

Lines changed: 86 additions & 3 deletions

File tree

‎src/model-variants.ts‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,19 @@ export function buildModelVariants(item: ModelListItem): Record<string, CursorVa
8888
}
8989

9090
for (const value of values) {
91-
// Key by the bare value (e.g. "high"); prefix with the param id only
92-
// when two params share a value (e.g. reasoning-low vs effort-low).
93-
const key = out[value] === undefined ? value : `${param.id}-${value}`;
91+
// `none` means reasoning OFF — the model's default when no variant is
92+
// selected. Surfacing it as a selectable variant is meaningless (you
93+
// get it by picking nothing), so skip it. Standard providers
94+
// (models.dev) include `none` in their effort values, but the
95+
// no-variant-selected state already represents it.
96+
if (value === "none") continue;
97+
// Cursor labels the top reasoning tier "extra-high"; the opencode
98+
// standard (models.dev) calls it "xhigh". Use the standard label for
99+
// the variant key so the cycler is consistent across providers, but
100+
// keep the Cursor wire-format value ("extra-high") in the params sent
101+
// to the API.
102+
const displayKey = value === "extra-high" ? "xhigh" : value;
103+
const key = out[displayKey] === undefined ? displayKey : `${param.id}-${displayKey}`;
94104
out[key] = { params: { ...defaults, [param.id]: value } };
95105
}
96106
continue;

‎test/model-variants.test.ts‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,79 @@ describe("buildModelVariants", () => {
131131
});
132132
});
133133

134+
it("renames extra-high to xhigh in the variant key but keeps the wire value", () => {
135+
// Cursor labels the top reasoning tier "extra-high"; the opencode standard
136+
// (models.dev) calls it "xhigh". The variant KEY normalizes to xhigh so
137+
// the cycler is consistent across providers; the param VALUE sent to
138+
// Cursor's API stays "extra-high".
139+
const variants = buildModelVariants(
140+
model([{ id: "reasoning", values: [{ value: "low" }, { value: "extra-high" }] }]),
141+
);
142+
expect(variants).toEqual({
143+
low: { params: { reasoning: "low" } },
144+
xhigh: { params: { reasoning: "extra-high" } },
145+
});
146+
});
147+
148+
it("drops the 'none' reasoning value (it is the model default)", () => {
149+
// `none` = reasoning OFF, which is what you get by selecting no variant.
150+
// Surfacing it as a selectable entry is meaningless, so it is skipped.
151+
const variants = buildModelVariants(
152+
model([
153+
{ id: "reasoning", values: [{ value: "none" }, { value: "low" }, { value: "high" }] },
154+
]),
155+
);
156+
expect(variants).toEqual({
157+
low: { params: { reasoning: "low" } },
158+
high: { params: { reasoning: "high" } },
159+
});
160+
});
161+
162+
it("composes none-drop and extra-high rename for the real GPT shape", () => {
163+
// gpt-5.5 / gpt-5.4 catalog: reasoning=[none,low,medium,high,extra-high]
164+
// + fast. Expect: low, medium, high, xhigh (no none, extra-high→xhigh),
165+
// each effort variant bakes fast OFF, plus a standalone fast opt-in.
166+
const variants = buildModelVariants(
167+
model([
168+
{
169+
id: "reasoning",
170+
values: [
171+
{ value: "none" },
172+
{ value: "low" },
173+
{ value: "medium" },
174+
{ value: "high" },
175+
{ value: "extra-high" },
176+
],
177+
},
178+
{ id: "fast", values: [{ value: "false" }, { value: "true" }] },
179+
]),
180+
);
181+
expect(variants).toEqual({
182+
low: { params: { reasoning: "low", fast: "false" } },
183+
medium: { params: { reasoning: "medium", fast: "false" } },
184+
high: { params: { reasoning: "high", fast: "false" } },
185+
xhigh: { params: { reasoning: "extra-high", fast: "false" } },
186+
fast: { params: { fast: "true" } },
187+
});
188+
});
189+
190+
it("prefixes the display key on a collision involving extra-high", () => {
191+
// Defensive: if two reasoning params both resolve to the xhigh display key
192+
// (one via the real "xhigh" value, one via "extra-high"→xhigh), the second
193+
// is prefixed with its param id. No current catalog model hits this, but
194+
// the guard must hold.
195+
const variants = buildModelVariants(
196+
model([
197+
{ id: "effort", values: [{ value: "xhigh" }] },
198+
{ id: "reasoning", values: [{ value: "extra-high" }] },
199+
]),
200+
);
201+
expect(variants).toEqual({
202+
xhigh: { params: { effort: "xhigh" } },
203+
"reasoning-xhigh": { params: { reasoning: "extra-high" } },
204+
});
205+
});
206+
134207
it("surfaces a non-reasoning boolean (fast) as an opt-in toggle; ignores enum context", () => {
135208
// `fast` is Cursor's fast-tier toggle. It is not a reasoning level, but the
136209
// user must be able to opt INTO it from the picker (default is off, sent

0 commit comments

Comments
 (0)