From cee01a56caf1979b76a46f702a77fd9787ed3dcc Mon Sep 17 00:00:00 2001 From: pierre Date: Mon, 26 Jan 2026 12:16:36 +0100 Subject: [PATCH 1/2] feat(command): add model command to get/set the model --- lua/copilot/api/init.lua | 10 ++ lua/copilot/client/config.lua | 5 + lua/copilot/client/utils.lua | 4 +- lua/copilot/model.lua | 256 ++++++++++++++++++++++++++++++++++ plugin/copilot.lua | 5 +- 5 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 lua/copilot/model.lua diff --git a/lua/copilot/api/init.lua b/lua/copilot/api/init.lua index a9789439..8b665648 100644 --- a/lua/copilot/api/init.lua +++ b/lua/copilot/api/init.lua @@ -154,4 +154,14 @@ end ---@alias copilot_window_show_document { uri: string, external?: boolean, takeFocus?: boolean, selection?: boolean } ---@alias copilot_window_show_document_result { success: boolean } +---@alias copilot_model { id: string, modelName: string, scopes: string[], preview?: boolean, default?: boolean } +---@alias copilot_models_data copilot_model[] + +---@return any|nil err +---@return copilot_models_data data +---@return table ctx +function M.get_models(client, callback) + return M.request(client, "copilot/models", {}, callback) +end + return M diff --git a/lua/copilot/client/config.lua b/lua/copilot/client/config.lua index 5c88d126..7bf682ab 100644 --- a/lua/copilot/client/config.lua +++ b/lua/copilot/client/config.lua @@ -124,6 +124,11 @@ function M.prepare_client_config(overrides, client) end require("copilot.nes").setup(lsp_client) + + -- Validate configured model on startup + if config.copilot_model and config.copilot_model ~= "" then + require("copilot.model").validate_current() + end end) end, on_exit = function(code, _, client_id) diff --git a/lua/copilot/client/utils.lua b/lua/copilot/client/utils.lua index f9ec2a46..587440ef 100644 --- a/lua/copilot/client/utils.lua +++ b/lua/copilot/client/utils.lua @@ -29,7 +29,9 @@ function M.get_workspace_configurations() filetypes = vim.tbl_deep_extend("keep", filetypes, client_ft.internal_filetypes) end - local copilot_model = config and config.copilot_model ~= "" and config.copilot_model or "" + -- Use model module to get the current model (supports runtime override) + local model = require("copilot.model") + local copilot_model = model.get_current_model() ---@type string[] local disabled_filetypes = vim.tbl_filter(function(ft) diff --git a/lua/copilot/model.lua b/lua/copilot/model.lua new file mode 100644 index 00000000..f1660184 --- /dev/null +++ b/lua/copilot/model.lua @@ -0,0 +1,256 @@ +local c = require("copilot.client") +local api = require("copilot.api") +local config = require("copilot.config") +local logger = require("copilot.logger") + +local M = {} + +--- Runtime override of the model (not persisted to user config) +--- When set, this takes precedence over config.copilot_model +---@type string|nil +M.selected_model = nil + +--- Get the currently active model ID +---@return string +function M.get_current_model() + return M.selected_model or config.copilot_model or "" +end + +--- Filter models that support completions +---@param models copilot_model[] +---@return copilot_model[] +local function get_completion_models(models) + return vim.tbl_filter(function(m) + return vim.tbl_contains(m.scopes or {}, "completion") + end, models) +end + +--- Format a model for display +---@param model copilot_model +---@return string +local function format_model(model, show_id) + local parts = { model.modelName } + if show_id then + table.insert(parts, "[" .. model.id .. "]") + end + local annotations = {} + + if model.default then + table.insert(annotations, "default") + end + if model.preview then + table.insert(annotations, "preview") + end + + if #annotations > 0 then + table.insert(parts, "(" .. table.concat(annotations, ", ") .. ")") + end + + return table.concat(parts, " ") +end + +--- Apply the selected model by notifying the LSP server +---@param model_id string +local function apply_model(model_id) + M.selected_model = model_id + + local client = c.get() + if client then + local utils = require("copilot.client.utils") + local configurations = utils.get_workspace_configurations() + api.notify_change_configuration(client, configurations) + logger.debug("Model changed to: " .. model_id) + end +end + +--- Interactive model selection using vim.ui.select +---@param opts? { force?: boolean, args?: string } +function M.select(opts) + opts = opts or {} + + local client = c.get() + if not client then + logger.notify("Copilot client not running") + return + end + + coroutine.wrap(function() + local err, models = api.get_models(client) + if err then + logger.notify("Failed to get models: " .. vim.inspect(err)) + return + end + + if not models or #models == 0 then + logger.notify("No models available") + return + end + + local completion_models = get_completion_models(models) + if #completion_models == 0 then + logger.notify("No completion models available") + return + end + + local current_model = M.get_current_model() + if #completion_models == 1 then + local model = completion_models[1] + local model_name = format_model(model) + logger.notify("Only one completion model available: " .. model_name) + if model.id ~= current_model then + apply_model(model.id) + logger.notify("Copilot model set to: " .. model_name) + else + logger.notify("Copilot model is already set to: " .. model_name) + end + return + end + + -- Sort models: default first, then by name + table.sort(completion_models, function(a, b) + if a.default and not b.default then + return true + end + if b.default and not a.default then + return false + end + return a.modelName < b.modelName + end) + + vim.ui.select(completion_models, { + prompt = "Select Copilot completion model:", + format_item = function(model) + local display = format_model(model) + if model.id == current_model then + display = display .. " [current]" + end + return display + end, + }, function(selected) + if not selected then + return + end + + apply_model(selected.id) + logger.notify("Copilot model set to: " .. format_model(selected)) + end) + end)() +end + +--- List available completion models +---@param opts? { force?: boolean, args?: string } +function M.list(opts) + opts = opts or {} + + local client = c.get() + if not client then + logger.notify("Copilot client not running") + return + end + + coroutine.wrap(function() + local err, models = api.get_models(client) + if err then + logger.notify("Failed to get models: " .. vim.inspect(err)) + return + end + + if not models or #models == 0 then + logger.notify("No models available") + return + end + + local completion_models = get_completion_models(models) + if #completion_models == 0 then + logger.notify("No completion models available") + return + end + + local current_model = M.get_current_model() + local lines = { "Available completion models:" } + + for _, model in ipairs(completion_models) do + local line = " " .. format_model(model, true) + if model.id == current_model then + line = line .. " <- current" + end + table.insert(lines, line) + end + + logger.notify(table.concat(lines, "\n")) + end)() +end + +--- Show the current model +---@param opts? { force?: boolean, args?: string } +function M.get(opts) + opts = opts or {} + + local current = M.get_current_model() + if current == "" then + logger.notify("No model configured (using server default)") + else + logger.notify("Current model: " .. current) + end +end + +--- Set the model programmatically +---@param opts { model?: string, force?: boolean, args?: string } +function M.set(opts) + opts = opts or {} + + local model_id = opts.model or opts.args + if not model_id or model_id == "" then + logger.notify("Usage: :Copilot model set ") + return + end + + apply_model(model_id) + logger.notify("Copilot model set to: " .. model_id) +end + +--- Validate the currently configured model against available models +--- Called on startup to warn if the configured model is invalid +function M.validate_current() + local configured_model = config.copilot_model + if not configured_model or configured_model == "" then + return -- No model configured, nothing to validate + end + + local client = c.get() + if not client then + return + end + + coroutine.wrap(function() + local err, models = api.get_models(client) + if err then + logger.debug("Failed to validate model: " .. vim.inspect(err)) + return + end + + if not models or #models == 0 then + return + end + + local completion_models = get_completion_models(models) + local valid_ids = vim.tbl_map(function(m) + return m.id + end, completion_models) + + if not vim.tbl_contains(valid_ids, configured_model) then + local valid_list = table.concat(valid_ids, ", ") + logger.warn( + string.format( + "Configured copilot_model '%s' is not a valid completion model. Available: %s", + configured_model, + valid_list + ) + ) + else + logger.debug("Configured model '" .. configured_model .. "' is valid") + end + end)() +end + +return M diff --git a/plugin/copilot.lua b/plugin/copilot.lua index 1a042ea4..0128edd6 100644 --- a/plugin/copilot.lua +++ b/plugin/copilot.lua @@ -1,6 +1,7 @@ local completion_store = { - [""] = { "auth", "attach", "detach", "disable", "enable", "panel", "status", "suggestion", "toggle", "version" }, + [""] = { "auth", "attach", "detach", "disable", "enable", "model", "panel", "status", "suggestion", "toggle", "version" }, auth = { "signin", "signout", "info" }, + model = { "select", "list", "get", "set" }, panel = { "accept", "jump_next", "jump_prev", "open", "refresh", "toggle", "close", "is_open" }, suggestion = { "accept", @@ -34,6 +35,8 @@ vim.api.nvim_create_user_command("Copilot", function(opts) if not action_name then if mod_name == "auth" then action_name = "signin" + elseif mod_name == "model" then + action_name = "get" elseif mod_name == "panel" then action_name = "open" elseif mod_name == "suggestion" then From 68740ccd7743c3b584b6a0ac2dce817cec899fae Mon Sep 17 00:00:00 2001 From: pierre Date: Wed, 28 Jan 2026 17:14:32 +0100 Subject: [PATCH 2/2] fix lint (try) --- lua/copilot/model.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/copilot/model.lua b/lua/copilot/model.lua index f1660184..a216fb31 100644 --- a/lua/copilot/model.lua +++ b/lua/copilot/model.lua @@ -66,7 +66,7 @@ end --- Interactive model selection using vim.ui.select ---@param opts? { force?: boolean, args?: string } function M.select(opts) - opts = opts or {} + _ = opts or {} local client = c.get() if not client then @@ -140,7 +140,7 @@ end --- List available completion models ---@param opts? { force?: boolean, args?: string } function M.list(opts) - opts = opts or {} + _ = opts or {} local client = c.get() if not client then @@ -184,7 +184,7 @@ end --- Show the current model ---@param opts? { force?: boolean, args?: string } function M.get(opts) - opts = opts or {} + _ = opts or {} local current = M.get_current_model() if current == "" then