Skip to content

Commit e400396

Browse files
authored
feat(adapters): show copilot multipliers and refactor changing adapters (#2427)
1 parent 4abba49 commit e400396

File tree

8 files changed

+1088
-155
lines changed

8 files changed

+1088
-155
lines changed

lua/codecompanion/adapters/http/copilot/get_models.lua

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ local function fetch_async(adapter, provided_token)
7676

7777
local base_url = (fresh_token.endpoints and fresh_token.endpoints.api) or "https://api.githubcopilot.com"
7878
local url = base_url .. "/models"
79+
7980
local headers = vim.deepcopy(_cached_adapter.headers or {})
8081
headers["Authorization"] = "Bearer " .. fresh_token.copilot_token
82+
headers["X-Github-Api-Version"] = "2025-10-01"
8183

8284
-- Async request via plenary.curl with a callback
8385
local ok, err = pcall(function()
@@ -115,26 +117,50 @@ local function fetch_async(adapter, provided_token)
115117
end
116118
end
117119

118-
if model.model_picker_enabled and model.capabilities and model.capabilities.type == "chat" then
120+
if model.model_picker_enabled then
119121
local choice_opts = {}
122+
local limits = {}
123+
local billing = {}
120124

121-
if model.capabilities.supports and model.capabilities.supports.streaming then
122-
choice_opts.can_stream = true
123-
end
124-
if model.capabilities.supports and model.capabilities.supports.tool_calls then
125-
choice_opts.can_use_tools = true
125+
if model.capabilities then
126+
if type(model.capabilities.type) == "string" and model.capabilities.type ~= "chat" then
127+
log:debug("Copilot Adapter: Skipping non-chat model '%s'", model.id)
128+
goto continue
129+
end
130+
if type(model.capabilities.type) == "table" and not vim.tbl_contains(model.capabilities.type, "chat") then
131+
log:debug("Copilot Adapter: Skipping non-chat model '%s'", model.id)
132+
goto continue
133+
end
134+
if model.capabilities.supports and model.capabilities.supports.streaming then
135+
choice_opts.can_stream = true
136+
end
137+
if model.capabilities.supports and model.capabilities.supports.tool_calls then
138+
choice_opts.can_use_tools = true
139+
end
140+
if model.capabilities.supports and model.capabilities.supports.vision then
141+
choice_opts.has_vision = true
142+
end
143+
if model.capabilities.limits then
144+
limits.max_output_tokens = model.capabilities.limits.max_output_tokens
145+
limits.max_prompt_tokens = model.capabilities.limits.max_prompt_tokens
146+
end
126147
end
127-
if model.capabilities.supports and model.capabilities.supports.vision then
128-
choice_opts.has_vision = true
148+
149+
if model.billing then
150+
billing.is_premium = model.billing.is_premium
151+
billing.multiplier = model.billing.multiplier
129152
end
130153

131154
models[model.id] = {
155+
billing = billing,
132156
endpoint = internal_endpoint,
133-
vendor = model.vendor,
134157
formatted_name = model.name,
158+
limits = limits,
135159
opts = choice_opts,
160+
vendor = model.vendor,
136161
}
137162
end
163+
138164
::continue::
139165
end
140166

lua/codecompanion/adapters/http/copilot/init.lua

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,15 @@ return {
166166

167167
local result = handlers(self).form_messages(self, messages)
168168

169-
-- Transform reasoning data and merge consecutive LLM messages for Copilot API
169+
-- For gemini-3, merge consecutive LLM messages and ensure that reasoning
170+
-- data is transformed. This enables consecutive tool calls to be made
170171
if result.messages then
171172
local merged = {}
172173
local i = 1
173174
while i <= #result.messages do
174175
local current = result.messages[i]
175176

176-
-- Transform reasoning data
177+
-- gemini-3 requires reasoning_text and reasoning_opaque fields
177178
if current.reasoning then
178179
if current.reasoning.content then
179180
current.reasoning_text = current.reasoning.content
@@ -184,15 +185,14 @@ return {
184185
current.reasoning = nil
185186
end
186187

187-
-- Check if next message is also from LLM and has tool_calls but no content
188-
-- This indicates tool calls that should be merged with the previous message
188+
-- From investigating Copilot Chat's output, tool_calls are merged
189+
-- into a single message per role with reasoning data
189190
if
190191
i < #result.messages
191192
and result.messages[i + 1].role == current.role
192193
and result.messages[i + 1].tool_calls
193194
and not result.messages[i + 1].content
194195
then
195-
-- Merge tool_calls from next message into current
196196
current.tool_calls = result.messages[i + 1].tool_calls
197197
i = i + 1 -- Skip the next message since we merged it
198198
end

lua/codecompanion/adapters/http/deepseek.lua

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,11 @@ return {
132132
---@type string|fun(): string
133133
default = "deepseek-reasoner",
134134
choices = {
135-
["deepseek-reasoner"] = { formatted_name = "DeepSeek", opts = { can_reason = true, can_use_tools = false } },
136-
["deepseek-chat"] = { formatted_name = "DeepSeek", opts = { can_use_tools = true } },
135+
["deepseek-reasoner"] = {
136+
formatted_name = "DeepSeek Reasoner",
137+
opts = { can_reason = true, can_use_tools = false },
138+
},
139+
["deepseek-chat"] = { formatted_name = "DeepSeek Chat", opts = { can_use_tools = true } },
137140
},
138141
},
139142
---@type CodeCompanion.Schema

lua/codecompanion/strategies/chat/keymaps.lua

Lines changed: 1 addition & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -515,143 +515,7 @@ M.previous_header = {
515515
M.change_adapter = {
516516
desc = "Change the adapter",
517517
callback = function(chat)
518-
if config.display.chat.show_settings then
519-
return utils.notify("Adapter can't be changed when `display.chat.show_settings = true`", vim.log.levels.WARN)
520-
end
521-
522-
local function select_opts(prompt, conditional)
523-
return {
524-
prompt = prompt,
525-
kind = "codecompanion.nvim",
526-
format_item = function(item)
527-
if conditional == item then
528-
return "* " .. item
529-
end
530-
return " " .. item
531-
end,
532-
}
533-
end
534-
535-
--TODO: Remove `config.adapters` in V18.0.0
536-
local adapters = vim.tbl_deep_extend(
537-
"force",
538-
{},
539-
vim.deepcopy(config.adapters.acp),
540-
vim.deepcopy(config.adapters.http),
541-
vim.deepcopy(config.adapters)
542-
)
543-
local current_adapter = chat.adapter.name
544-
local current_model
545-
546-
if current_adapter.type == "http" then
547-
current_model = vim.deepcopy(chat.adapter.schema.model.default)
548-
end
549-
550-
local adapters_list = vim
551-
.iter(adapters)
552-
:filter(function(adapter)
553-
-- Clear out the acp and http keys
554-
return adapter ~= "opts" and adapter ~= "acp" and adapter ~= "http" and adapter ~= current_adapter
555-
end)
556-
:map(function(adapter, _)
557-
return adapter
558-
end)
559-
:totable()
560-
561-
table.sort(adapters_list)
562-
table.insert(adapters_list, 1, current_adapter)
563-
564-
vim.ui.select(adapters_list, select_opts("Select Adapter", current_adapter), function(selected_adapter)
565-
if not selected_adapter then
566-
return
567-
end
568-
569-
if current_adapter ~= selected_adapter then
570-
chat.acp_connection = nil
571-
chat:change_adapter(selected_adapter)
572-
end
573-
574-
-- Update the system prompt
575-
local system_prompt = config.strategies.chat.opts.system_prompt
576-
if type(system_prompt) == "function" then
577-
if chat.messages[1] and chat.messages[1].role == "system" then
578-
chat.messages[1].content = system_prompt(chat:make_system_prompt_ctx())
579-
end
580-
end
581-
582-
-- Select a model
583-
if chat.adapter.type == "http" then
584-
local models = chat.adapter.schema.model.choices
585-
if not config.adapters.http.opts.show_model_choices then
586-
models = { chat.adapter.schema.model.default }
587-
end
588-
if type(models) == "function" then
589-
models = models(chat.adapter, { async = false })
590-
end
591-
if not models or vim.tbl_count(models) < 2 then
592-
return
593-
end
594-
595-
local new_model = chat.adapter.schema.model.default
596-
if type(new_model) == "function" then
597-
new_model = new_model(chat.adapter)
598-
end
599-
600-
models = vim
601-
.iter(models)
602-
:map(function(model, value)
603-
if type(model) == "string" then
604-
return model
605-
else
606-
return value -- This is for the table entry case
607-
end
608-
end)
609-
:filter(function(model)
610-
return model ~= new_model
611-
end)
612-
:totable()
613-
table.sort(models)
614-
table.insert(models, 1, new_model)
615-
616-
vim.ui.select(models, select_opts("Select Model", new_model), function(selected_model)
617-
if not selected_model then
618-
return
619-
end
620-
chat:apply_model(selected_model)
621-
end)
622-
end
623-
624-
-- Select a command
625-
if chat.adapter.type == "acp" then
626-
local commands = chat.adapter.commands
627-
if not commands or vim.tbl_count(commands) < 2 then
628-
return
629-
end
630-
631-
commands = vim
632-
.iter(commands)
633-
:map(function(key, _)
634-
if type(key) == "string" then
635-
return key
636-
end
637-
end)
638-
:filter(function(key)
639-
return key ~= "selected"
640-
end)
641-
:totable()
642-
table.sort(commands)
643-
644-
vim.ui.select(commands, select_opts("Select a Command", commands), function(selected_command)
645-
if not selected_command then
646-
return
647-
end
648-
local selected = chat.adapter.commands[selected_command]
649-
chat.adapter.commands.selected = selected
650-
utils.fire("ChatModel", { bufnr = chat.bufnr, model = selected })
651-
chat:update_metadata()
652-
end)
653-
end
654-
end)
518+
require("codecompanion.strategies.chat.keymaps.change_adapter").callback(chat)
655519
end,
656520
}
657521

0 commit comments

Comments
 (0)