Skip to content

Commit 3ecb050

Browse files
committed
feat(opencode): add model variant support and cycle command
Add per-model variant support and UI/commands to select and cycle variants. - Add lua/opencode/model_state.lua to persist favorites, recent and selected variants - Add lua/opencode/variant_picker.lua and expose configure_variant / cycle_variant - Thread variant through API/client, core send flow, config, types, utils and UI footer - Update model picker, keymaps, highlights and state to load/save the current variant Enable selecting, remembering, and cycling model variants for the active model.
1 parent 9d84f27 commit 3ecb050

14 files changed

Lines changed: 448 additions & 136 deletions

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ require('opencode').setup({
127127
['<leader>os'] = { 'select_session' }, -- Select and load a opencode session
128128
['<leader>oR'] = { 'rename_session' }, -- Rename current session
129129
['<leader>op'] = { 'configure_provider' }, -- Quick provider and model switch from predefined list
130+
['<leader>oV'] = { 'configure_variant' }, -- Switch model variant for the current model
130131
['<leader>oz'] = { 'toggle_zoom' }, -- Zoom in/out on the Opencode windows
131132
['<leader>ov'] = { 'paste_image'}, -- Paste image from clipboard into current session
132133
['<leader>od'] = { 'diff_open' }, -- Opens a diff tab of a modified file since the last opencode prompt
@@ -161,6 +162,7 @@ require('opencode').setup({
161162
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' } }, -- Navigate to previous prompt in history
162163
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' } }, -- Navigate to next prompt in history
163164
['<M-m>'] = { 'switch_mode' }, -- Switch between modes (build/plan)
165+
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' } }, -- Cycle through available model variants
164166
},
165167
output_window = {
166168
['<esc>'] = { 'close' }, -- Close UI windows
@@ -169,6 +171,7 @@ require('opencode').setup({
169171
['[['] = { 'prev_message' }, -- Navigate to previous message in the conversation
170172
['<tab>'] = { 'toggle_pane', mode = { 'n', 'i' } }, -- Toggle between input and output panes
171173
['i'] = { 'focus_input', 'n' }, -- Focus on input window and enter insert mode at the end of the input from the output window
174+
['<M-r>'] = { 'cycle_variant', mode = { 'n' } }, -- Cycle through available model variants
172175
['<leader>oS'] = { 'select_child_session' }, -- Select and load a child session
173176
['<leader>oD'] = { 'debug_message' }, -- Open raw message in new buffer for debugging
174177
['<leader>oO'] = { 'debug_output' }, -- Open raw output in new buffer for debugging
@@ -377,6 +380,18 @@ In the model picker, press **`<C-f>`** to toggle the currently selected model as
377380

378381
No configuration is needed - the plugin respects and updates the OpenCode CLI format automatically.
379382

383+
### Model Variants
384+
385+
Some models support multiple variants (e.g., different context window sizes or optimization modes). The plugin provides convenient ways to switch between available variants for the currently active model.
386+
387+
#### Switching Variants
388+
389+
- **Via picker**: Press `<leader>oV` to open the variant picker showing all available variants for the current model
390+
- **Via cycling**: Press `<M-r>` (Alt+R) in the input or output window to cycle through available variants
391+
- **Via slash command**: Type `/variant` in the input window
392+
393+
When you switch variants, the plugin remembers your selection per model, so the next time you use that model, it will automatically use the last selected variant.
394+
380395
### UI icons (disable emojis or customize)
381396

382397
By default, opencode.nvim uses emojis for icons in the UI. If you prefer a plain, emoji-free interface, you can switch to the `text` preset or override icons individually.
@@ -552,6 +567,8 @@ The plugin provides the following actions that can be triggered via keymaps, com
552567
| Open timeline picker (navigate/undo/redo/fork to message) | `<leader>oT` | `:Opencode timeline` | `require('opencode.api').timeline()` |
553568
| Browse code references from conversation | `gr` (window) | `:Opencode references` / `/references` | `require('opencode.api').references()` |
554569
| Configure provider and model | `<leader>op` | `:Opencode configure provider` | `require('opencode.api').configure_provider()` |
570+
| Configure model variant | `<leader>oV` | `:Opencode variant` / `/variant` | `require('opencode.api').configure_variant()` |
571+
| Cycle through model variants | `<M-r>` (window) | - | `require('opencode.api').cycle_variant()` |
555572
| Open diff view of changes | `<leader>od` | `:Opencode diff open` | `require('opencode.api').diff_open()` |
556573
| Navigate to next file diff | `<leader>o]` | `:Opencode diff next` | `require('opencode.api').diff_next()` |
557574
| Navigate to previous file diff | `<leader>o[` | `:Opencode diff prev` | `require('opencode.api').diff_prev()` |
@@ -720,6 +737,7 @@ You can run predefined user commands and built-in slash commands from the input
720737
- `/help` — Show help
721738
- `/mcp` — Show MCP servers
722739
- `/models` — Switch provider/model
740+
- `/variant` — Switch model variant
723741
- `/sessions` — Switch session
724742
- `/child-sessions` — Switch to a child session
725743
- `/agent` — Switch agent/mode

lua/opencode/api.lua

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,14 @@ function M.configure_provider()
8383
core.configure_provider()
8484
end
8585

86+
function M.configure_variant()
87+
core.configure_variant()
88+
end
89+
90+
function M.cycle_variant()
91+
core.cycle_variant()
92+
end
93+
8694
function M.cancel()
8795
core.cancel()
8896
end
@@ -1205,6 +1213,11 @@ M.commands = {
12051213
fn = M.configure_provider,
12061214
},
12071215

1216+
variant = {
1217+
desc = 'Switch model variant',
1218+
fn = M.configure_variant,
1219+
},
1220+
12081221
run = {
12091222
desc = 'Run prompt in current session',
12101223
fn = function(args)
@@ -1334,6 +1347,7 @@ M.slash_commands_map = {
13341347
['/history'] = { fn = M.select_history, desc = 'Select from history' },
13351348
['/mcp'] = { fn = M.mcp, desc = 'Show MCP server configuration' },
13361349
['/models'] = { fn = M.configure_provider, desc = 'Switch provider/model' },
1350+
['/variant'] = { fn = M.configure_variant, desc = 'Switch model variant' },
13371351
['/new'] = { fn = M.open_input_new_session, desc = 'Create new session' },
13381352
['/redo'] = { fn = M.redo, desc = 'Redo last action' },
13391353
['/sessions'] = { fn = M.select_session, desc = 'Select session' },
@@ -1365,6 +1379,7 @@ M.legacy_command_map = {
13651379
OpencodeSelectChildSession = 'session child',
13661380
OpencodeTogglePane = 'toggle_pane',
13671381
OpencodeConfigureProvider = 'models',
1382+
OpencodeConfigureVariant = 'variant',
13681383
OpencodeRun = 'run',
13691384
OpencodeRunNewSession = 'run_new',
13701385
OpencodeDiff = 'diff open',

lua/opencode/api_client.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ end
235235

236236
--- Create and send a new message to a session
237237
--- @param id string Session ID (required)
238-
--- @param message_data {messageID?: string, model?: {providerID: string, modelID: string}, agent?: string, system?: string, tools?: table<string, boolean>, parts: OpencodeMessagePart[]} Message creation data
238+
--- @param message_data {messageID?: string, model?: {providerID: string, modelID: string}, agent?: string, variant?: string, system?: string, tools?: table<string, boolean>, parts: OpencodeMessagePart[]} Message creation data
239239
--- @param directory string|nil Directory path
240240
--- @return Promise<{info: MessageInfo, parts: OpencodeMessagePart[]}>
241241
function OpencodeApiClient:create_message(id, message_data, directory)

lua/opencode/config.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ M.defaults = {
2626
['<leader>os'] = { 'select_session', desc = 'Select session' },
2727
['<leader>oR'] = { 'rename_session', desc = 'Rename session' },
2828
['<leader>op'] = { 'configure_provider', desc = 'Configure provider' },
29+
['<leader>oV'] = { 'configure_variant', desc = 'Configure model variant' },
2930
['<leader>oz'] = { 'toggle_zoom', desc = 'Toggle zoom' },
3031
['<leader>ov'] = { 'paste_image', desc = 'Paste image from clipboard' },
3132
['<leader>od'] = { 'diff_open', desc = 'Open diff view' },
@@ -55,6 +56,7 @@ M.defaults = {
5556
['i'] = { 'focus_input' },
5657
['gr'] = { 'references', desc = 'Browse code references' },
5758
['<M-i>'] = { 'toggle_input', mode = { 'n' }, desc = 'Toggle input window' },
59+
['<M-r>'] = { 'cycle_variant', mode = { 'n' } },
5860
['<leader>oS'] = { 'select_child_session' },
5961
['<leader>oD'] = { 'debug_message' },
6062
['<leader>oO'] = { 'debug_output' },
@@ -73,6 +75,7 @@ M.defaults = {
7375
['<up>'] = { 'prev_prompt_history', mode = { 'n', 'i' } },
7476
['<down>'] = { 'next_prompt_history', mode = { 'n', 'i' } },
7577
['<M-m>'] = { 'switch_mode', mode = { 'n', 'i' } },
78+
['<M-r>'] = { 'cycle_variant', mode = { 'n', 'i' } },
7679
['<M-i>'] = { 'toggle_input', mode = { 'n', 'i' }, desc = 'Toggle input window' },
7780
['gr'] = { 'references', desc = 'Browse code references' },
7881
['<leader>oS'] = { 'select_child_session' },

lua/opencode/config_file.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ function M.get_opencode_providers()
6363
end)
6464
end
6565

66+
--- Get model information for a specific provider and model
67+
--- @param provider string Provider ID
68+
--- @param model string Model ID
69+
--- @return OpencodeModel|nil Model information with variants
6670
M.get_model_info = function(provider, model)
6771
local providers_response = M.get_opencode_providers():peek()
6872

lua/opencode/core.lua

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,18 @@ M.send_message = Promise.async(function(prompt, opts)
154154
context.load()
155155
opts.model = opts.model or M.initialize_current_model():await()
156156
opts.agent = opts.agent or state.current_mode or config.default_mode
157-
157+
opts.variant = opts.variant or state.current_variant
158158
local params = {}
159159

160160
if opts.model then
161161
local provider, model = opts.model:match('^(.-)/(.+)$')
162162
params.model = { providerID = provider, modelID = model }
163163
state.current_model = opts.model
164+
165+
if opts.variant then
166+
params.variant = opts.variant
167+
state.current_variant = opts.variant
168+
end
164169
end
165170

166171
if opts.agent then
@@ -255,7 +260,7 @@ function M.before_run(opts)
255260
end
256261

257262
function M.configure_provider()
258-
require('opencode.provider').select(function(selection)
263+
require('opencode.model_picker').select(function(selection)
259264
if not selection then
260265
if state.windows then
261266
ui.focus_input()
@@ -268,11 +273,101 @@ function M.configure_provider()
268273
if state.windows then
269274
ui.focus_input()
270275
else
271-
vim.notify('Changed provider to ' .. selection.display, vim.log.levels.INFO)
276+
vim.notify('Changed provider to ' .. model_str, vim.log.levels.INFO)
277+
end
278+
end)
279+
end
280+
281+
function M.configure_variant()
282+
require('opencode.variant_picker').select(function(selection)
283+
if not selection then
284+
if state.windows then
285+
ui.focus_input()
286+
end
287+
return
288+
end
289+
290+
state.current_variant = selection.name
291+
292+
-- Save variant to model state
293+
if state.current_model then
294+
local provider, model = state.current_model:match('^(.-)/(.+)$')
295+
if provider and model then
296+
local model_state = require('opencode.model_state')
297+
model_state.set_variant(provider, model, selection.name)
298+
end
299+
end
300+
301+
if state.windows then
302+
ui.focus_input()
303+
else
304+
vim.notify('Changed variant to ' .. selection.name, vim.log.levels.INFO)
272305
end
273306
end)
274307
end
275308

309+
M.cycle_variant = Promise.async(function()
310+
if not state.current_model then
311+
vim.notify('No model selected', vim.log.levels.WARN)
312+
return
313+
end
314+
315+
local provider, model = state.current_model:match('^(.-)/(.+)$')
316+
if not provider or not model then
317+
return
318+
end
319+
320+
local config_file = require('opencode.config_file')
321+
local model_info = config_file.get_model_info(provider, model)
322+
323+
if not model_info or not model_info.variants then
324+
vim.notify('Current model does not support variants', vim.log.levels.WARN)
325+
return
326+
end
327+
328+
local variants = {}
329+
for variant_name, _ in pairs(model_info.variants) do
330+
table.insert(variants, variant_name)
331+
end
332+
333+
table.sort(variants, function(a, b)
334+
local priority = { low = 1, medium = 2, high = 3 }
335+
local a_priority = priority[a] or 99
336+
local b_priority = priority[b] or 99
337+
if a_priority ~= b_priority then
338+
return a_priority < b_priority
339+
end
340+
return a < b
341+
end)
342+
343+
if #variants == 0 then
344+
return
345+
end
346+
347+
local total_count = #variants + 1
348+
349+
local current_index
350+
if state.current_variant == nil then
351+
current_index = total_count
352+
else
353+
current_index = util.index_of(variants, state.current_variant) or 0
354+
end
355+
356+
local next_index = (current_index % total_count) + 1
357+
358+
local next_variant
359+
if next_index > #variants then
360+
next_variant = nil
361+
else
362+
next_variant = variants[next_index]
363+
end
364+
365+
state.current_variant = next_variant
366+
367+
local model_state = require('opencode.model_state')
368+
model_state.set_variant(provider, model, next_variant)
369+
end)
370+
276371
M.cancel = Promise.async(function()
277372
if state.windows and state.active_session then
278373
if state.is_running() then
@@ -461,6 +556,23 @@ function M.setup()
461556
state.subscribe('opencode_server', on_opencode_server)
462557
state.subscribe('user_message_count', M._on_user_message_count_change)
463558
state.subscribe('pending_permissions', M._on_current_permission_change)
559+
state.subscribe('current_model', function(key, new_val, old_val)
560+
if new_val ~= old_val then
561+
state.current_variant = nil
562+
563+
-- Load saved variant for the new model
564+
if new_val then
565+
local provider, model = new_val:match('^(.-)/(.+)$')
566+
if provider and model then
567+
local model_state = require('opencode.model_state')
568+
local saved_variant = model_state.get_variant(provider, model)
569+
if saved_variant then
570+
state.current_variant = saved_variant
571+
end
572+
end
573+
end
574+
end
575+
end)
464576

465577
vim.schedule(function()
466578
M.opencode_ok()

0 commit comments

Comments
 (0)