Skip to content
Open
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
10 changes: 10 additions & 0 deletions lua/copilot/api/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions lua/copilot/client/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion lua/copilot/client/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
256 changes: 256 additions & 0 deletions lua/copilot/model.lua
Original file line number Diff line number Diff line change
@@ -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 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 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 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 <model-id>")
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
5 changes: 4 additions & 1 deletion plugin/copilot.lua
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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
Expand Down