@@ -13,38 +13,83 @@ export interface CursorVariant {
1313const REASONING_PARAM = / t h i n k | r e a s o n | e f f o r t / i;
1414const BOOLEAN_VALUES = new Set ( [ "true" , "false" ] ) ;
1515
16+ function paramValues ( param : NonNullable < ModelListItem [ "parameters" ] > [ number ] ) : string [ ] {
17+ return ( param . values ?? [ ] ) . map ( ( v ) => v . value ) ;
18+ }
19+
20+ function isBooleanParam ( values : string [ ] ) : boolean {
21+ return values . length > 0 && values . every ( ( v ) => BOOLEAN_VALUES . has ( v ) ) ;
22+ }
23+
24+ /**
25+ * Params opencode must send by DEFAULT for this model — i.e. when the user has
26+ * NOT picked a variant. Non-reasoning boolean toggles (notably Cursor's `fast`)
27+ * are pinned OFF here so the provider never silently inherits Cursor's
28+ * server-side default, which is `fast: true` for several models (composer-*,
29+ * gpt-*-codex). The user opts back IN via the matching picker variant.
30+ *
31+ * Seeded into each model's opencode `options.params` (see `toOpencodeModels` /
32+ * `buildModelV2Map`); {@link resolveControls} merges it into the request.
33+ */
34+ export function defaultModelParams ( item : ModelListItem ) : Record < string , string > {
35+ const out : Record < string , string > = { } ;
36+ for ( const param of item . parameters ?? [ ] ) {
37+ if ( REASONING_PARAM . test ( param . id ) ) continue ;
38+ if ( isBooleanParam ( paramValues ( param ) ) ) out [ param . id ] = "false" ;
39+ }
40+ return out ;
41+ }
42+
1643/**
1744 * Derive opencode model variants from a Cursor model's parameters so the
18- * variant picker can expose thinking/reasoning levels. Each variant's object is
19- * exactly what {@link resolveControls} consumes. Plan mode is NOT a variant:
20- * opencode's plan agent (Tab) is mapped to Cursor's plan mode by the plugin's
21- * `chat.params` hook.
45+ * variant picker can expose thinking/reasoning levels plus the `fast` toggle.
46+ * Each variant's object is exactly what {@link resolveControls} consumes. Plan
47+ * mode is NOT a variant: opencode's plan agent (Tab) is mapped to Cursor's plan
48+ * mode by the plugin's `chat.params` hook.
49+ *
50+ * Every variant for a fast-capable model carries an explicit `fast` value
51+ * (reasoning variants pin it OFF via {@link defaultModelParams}; the `fast`
52+ * variant turns it ON) so a selection never depends on Cursor's server-side
53+ * default for an omitted param.
2254 */
2355export function buildModelVariants ( item : ModelListItem ) : Record < string , CursorVariant > {
2456 const out : Record < string , CursorVariant > = { } ;
57+ // Non-reasoning boolean defaults (e.g. { fast: "false" }), pinned into every
58+ // reasoning variant so picking a reasoning level never re-enables fast.
59+ const defaults = defaultModelParams ( item ) ;
2560
2661 for ( const param of item . parameters ?? [ ] ) {
27- if ( ! REASONING_PARAM . test ( param . id ) ) continue ;
28- const values = ( param . values ?? [ ] ) . map ( ( v ) => v . value ) ;
62+ const values = paramValues ( param ) ;
2963 if ( values . length === 0 ) continue ;
64+ const boolean = isBooleanParam ( values ) ;
65+
66+ if ( REASONING_PARAM . test ( param . id ) ) {
67+ if ( boolean ) {
68+ // Boolean toggle (e.g. thinking=["false","true"]). Literal true/false
69+ // variant names are meaningless in the picker — surface a single
70+ // variant named after the param that switches it on. "Off" is the
71+ // model's default (no variant selected).
72+ if ( values . includes ( "true" ) ) {
73+ out [ param . id . toLowerCase ( ) ] = { params : { ...defaults , [ param . id ] : "true" } } ;
74+ }
75+ continue ;
76+ }
3077
31- if ( values . every ( ( v ) => BOOLEAN_VALUES . has ( v ) ) ) {
32- // Boolean toggle (e.g. thinking=["false","true"]). Literal true/false
33- // variant names are meaningless in the picker — surface a single
34- // variant named after the param that switches it on. "Off" is the
35- // model's default (no variant selected).
36- if ( values . includes ( "true" ) ) {
37- out [ param . id . toLowerCase ( ) ] = { params : { [ param . id ] : "true" } } ;
78+ for ( const value of values ) {
79+ // Key by the bare value (e.g. "high"); prefix with the param id only
80+ // when two params share a value (e.g. reasoning-low vs effort-low).
81+ const key = out [ value ] === undefined ? value : `${ param . id } -${ value } ` ;
82+ out [ key ] = { params : { ...defaults , [ param . id ] : value } } ;
3883 }
3984 continue ;
4085 }
4186
42- for ( const value of values ) {
43- // Key by the bare value (e.g. "high"); prefix with the param id only
44- // when two params share a value (e.g. reasoning-low vs effort-low).
45- const key = out [ value ] === undefined ? value : `${ param . id } -${ value } ` ;
46- out [ key ] = { params : { [ param . id ] : value } } ;
87+ // Non-reasoning boolean toggle (e.g. Cursor's `fast`). Default is OFF (see
88+ // defaultModelParams); expose a single opt-in variant that turns it ON.
89+ if ( boolean && values . includes ( "true" ) ) {
90+ out [ param . id . toLowerCase ( ) ] = { params : { ...defaults , [ param . id ] : "true" } } ;
4791 }
92+ // Non-reasoning enum params (e.g. `context`) remain unsupported in the picker.
4893 }
4994
5095 return out ;
0 commit comments