Skip to content

Commit e231f96

Browse files
committed
wip
1 parent d99a4c5 commit e231f96

5 files changed

Lines changed: 198 additions & 25 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ local function fetch_async(adapter, opts)
159159
endpoint = internal_endpoint,
160160
formatted_name = model.name,
161161
limits = limits,
162+
meta = limits.context_window and { context_window = limits.context_window } or nil,
162163
opts = choice_opts,
163164
vendor = model.vendor,
164165
}

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -161,15 +161,6 @@ return {
161161
self.opts.vision = false
162162
end
163163

164-
if not self.meta then
165-
self.meta = {}
166-
end
167-
self.meta["context_window"] = 128000
168-
169-
if model_opts and model_opts.limits and model_opts.limits.context_window then
170-
self.meta["context_window"] = model_opts.limits.context_window
171-
end
172-
173164
return token.init(self)
174165
end,
175166

lua/codecompanion/adapters/http/ollama/get_models.lua

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ local _running = {}
1212

1313
local M = {}
1414

15-
---@type table<string, table<string, { formatted_name: string?, opts: {can_reason: boolean, has_vision: boolean, can_use_tools: boolean} }>>
15+
---@type table<string, table<string, { formatted_name: string?, meta: { context_window: number }?, opts: {can_reason: boolean, has_vision: boolean, can_use_tools: boolean} }>>
1616
local _cached_models = {}
1717

1818
---@alias OllamaGetModelsOpts {last?: boolean, async?: boolean}
@@ -45,10 +45,10 @@ local function build_headers(adapter)
4545
return headers
4646
end
4747

48-
---Parse capabilities from a model info response
48+
---Parse capabilities and metadata from a model info response
4949
---@param output table The curl response
50-
---@return { can_reason: boolean, can_use_tools: boolean, has_vision: boolean }
51-
local function parse_capabilities(output)
50+
---@return { can_reason: boolean, can_use_tools: boolean, has_vision: boolean }, { context_window: number }?
51+
local function parse_model_info(output)
5252
local opts = {}
5353
if output.status ~= 200 then
5454
return opts
@@ -64,7 +64,15 @@ local function parse_capabilities(output)
6464
opts.can_use_tools = vim.list_contains(capabilities, "tools")
6565
opts.has_vision = vim.list_contains(capabilities, "vision")
6666

67-
return opts
67+
local meta
68+
if json.model_info and json.details and json.details.family then
69+
local context_length = json.model_info[json.details.family .. ".context_length"]
70+
if context_length then
71+
meta = { context_window = context_length }
72+
end
73+
end
74+
75+
return opts, meta
6876
end
6977

7078
---Fetch model list and model info.
@@ -110,9 +118,11 @@ local function fetch_models(adapter)
110118
proxy = config.adapters.http.opts.proxy,
111119
timeout = CONSTANTS.TIMEOUT,
112120
callback = function(output)
121+
local opts, meta = parse_model_info(output)
113122
_cached_models[url][model_obj.name] = {
114123
formatted_name = model_obj.name,
115-
opts = parse_capabilities(output),
124+
meta = meta,
125+
opts = opts,
116126
}
117127
pending[model_obj.name] = nil
118128
if vim.tbl_isempty(pending) then

tests/adapters/http/copilot/test_models.lua

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,11 @@ T["copilot.models"]["choices() synchronous returns expected models"] = function(
4949
name = "Model One",
5050
vendor = "copilot",
5151
model_picker_enabled = true,
52-
capabilities = { type = "chat", supports = { streaming = true, tool_calls = true, vision = true } },
52+
capabilities = {
53+
type = "chat",
54+
supports = { streaming = true, tool_calls = true, vision = true },
55+
limits = { max_context_window_tokens = 200000, max_output_tokens = 64000 },
56+
},
5357
},
5458
{
5559
id = "model2",
@@ -91,20 +95,21 @@ T["copilot.models"]["choices() synchronous returns expected models"] = function(
9195
model1 = {
9296
billing = {},
9397
description = "Model One",
94-
limits = {},
95-
vendor = "copilot",
9698
endpoint = "completions",
9799
formatted_name = "Model One",
100+
limits = { context_window = 200000, max_output_tokens = 64000 },
101+
meta = { context_window = 200000 },
98102
opts = { can_stream = true, can_use_tools = true, has_vision = true },
103+
vendor = "copilot",
99104
},
100105
model2 = {
101106
billing = {},
102107
description = "Model Two",
103-
limits = {},
104-
vendor = "copilot",
105108
endpoint = "completions",
106109
formatted_name = "Model Two",
110+
limits = {},
107111
opts = {},
112+
vendor = "copilot",
108113
},
109114
}
110115

@@ -139,7 +144,11 @@ T["copilot.models"]["choices() async populates cache and returns later"] = funct
139144
name = "Model One",
140145
vendor = "copilot",
141146
model_picker_enabled = true,
142-
capabilities = { type = "chat", supports = { streaming = true, tool_calls = true, vision = true } },
147+
capabilities = {
148+
type = "chat",
149+
supports = { streaming = true, tool_calls = true, vision = true },
150+
limits = { max_context_window_tokens = 200000, max_output_tokens = 64000 },
151+
},
143152
},
144153
{
145154
id = "model2",
@@ -178,20 +187,21 @@ T["copilot.models"]["choices() async populates cache and returns later"] = funct
178187
model1 = {
179188
billing = {},
180189
description = "Model One",
181-
limits = {},
182-
vendor = "copilot",
183190
endpoint = "completions",
184191
formatted_name = "Model One",
192+
limits = { context_window = 200000, max_output_tokens = 64000 },
193+
meta = { context_window = 200000 },
185194
opts = { can_stream = true, can_use_tools = true, has_vision = true },
195+
vendor = "copilot",
186196
},
187197
model2 = {
188198
billing = {},
189199
description = "Model Two",
190-
limits = {},
191-
vendor = "copilot",
192200
endpoint = "completions",
193201
formatted_name = "Model Two",
202+
limits = {},
194203
opts = {},
204+
vendor = "copilot",
195205
},
196206
}
197207

tests/adapters/http/test_ollama.lua

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
local h = require("tests.helpers")
22
local adapter
33

4+
local child = MiniTest.new_child_neovim()
45
local new_set = MiniTest.new_set
56
T = new_set()
67

@@ -360,4 +361,164 @@ T["Ollama adapter"]["OLLAMA_HOST"]["fallback to localhost when OLLAMA_HOST is no
360361
h.eq("http://localhost:11434", adapter.env_replaced.url)
361362
end
362363

364+
T["Ollama get_models"] = new_set({
365+
hooks = {
366+
pre_case = function()
367+
h.child_start(child)
368+
child.lua([[
369+
h = require('tests.helpers')
370+
config = require('tests.config')
371+
require('codecompanion').setup(config)
372+
]])
373+
end,
374+
post_once = child.stop,
375+
},
376+
})
377+
378+
T["Ollama get_models"]["choices() extracts context_window into meta"] = function()
379+
local result = child.lua([[
380+
local get_models = require("codecompanion.adapters.http.ollama.get_models")
381+
local Curl = require("plenary.curl")
382+
383+
local tags_body = vim.json.encode({
384+
models = { { name = "llama3.1:latest" } },
385+
})
386+
local show_body = vim.json.encode({
387+
capabilities = { "completion" },
388+
details = {
389+
family = "llama",
390+
families = { "llama" },
391+
format = "gguf",
392+
parameter_size = "8.0B",
393+
parent_model = "",
394+
quantization_level = "Q4_0",
395+
},
396+
model_info = {
397+
["llama.context_length"] = 8192,
398+
},
399+
})
400+
401+
Curl.get = function(url, opts)
402+
if opts.callback then
403+
opts.callback({ status = 200, body = tags_body })
404+
end
405+
end
406+
Curl.post = function(url, opts)
407+
if opts.callback then
408+
opts.callback({ status = 200, body = show_body })
409+
end
410+
end
411+
412+
local adapter = require("codecompanion.adapters.http").extend("ollama", {
413+
schema = {
414+
model = {
415+
default = "llama3.1:latest",
416+
choices = { "llama3.1:latest" },
417+
},
418+
},
419+
})
420+
421+
return get_models.choices(adapter, { async = false })
422+
]])
423+
424+
h.eq({ context_window = 8192 }, result["llama3.1:latest"].meta)
425+
h.eq(false, result["llama3.1:latest"].opts.can_reason)
426+
h.eq(false, result["llama3.1:latest"].opts.can_use_tools)
427+
h.eq(false, result["llama3.1:latest"].opts.has_vision)
428+
end
429+
430+
T["Ollama get_models"]["choices() extracts capabilities into opts"] = function()
431+
local result = child.lua([[
432+
local get_models = require("codecompanion.adapters.http.ollama.get_models")
433+
local Curl = require("plenary.curl")
434+
435+
local tags_body = vim.json.encode({
436+
models = { { name = "qwq:latest" } },
437+
})
438+
local show_body = vim.json.encode({
439+
capabilities = { "completion", "thinking", "tools", "vision" },
440+
details = {
441+
family = "qwen3",
442+
families = { "qwen3" },
443+
format = "gguf",
444+
parameter_size = "32.8B",
445+
parent_model = "",
446+
quantization_level = "Q4_0",
447+
},
448+
model_info = {
449+
["qwen3.context_length"] = 40960,
450+
},
451+
})
452+
453+
Curl.get = function(url, opts)
454+
if opts.callback then
455+
opts.callback({ status = 200, body = tags_body })
456+
end
457+
end
458+
Curl.post = function(url, opts)
459+
if opts.callback then
460+
opts.callback({ status = 200, body = show_body })
461+
end
462+
end
463+
464+
local adapter = require("codecompanion.adapters.http").extend("ollama", {
465+
schema = {
466+
model = {
467+
default = "qwq:latest",
468+
choices = { "qwq:latest" },
469+
},
470+
},
471+
})
472+
473+
return get_models.choices(adapter, { async = false })
474+
]])
475+
476+
h.eq({ context_window = 40960 }, result["qwq:latest"].meta)
477+
h.eq(true, result["qwq:latest"].opts.can_reason)
478+
h.eq(true, result["qwq:latest"].opts.can_use_tools)
479+
h.eq(true, result["qwq:latest"].opts.has_vision)
480+
end
481+
482+
T["Ollama get_models"]["choices() handles missing model_info gracefully"] = function()
483+
local result = child.lua([[
484+
local get_models = require("codecompanion.adapters.http.ollama.get_models")
485+
local Curl = require("plenary.curl")
486+
487+
local tags_body = vim.json.encode({
488+
models = { { name = "custom:latest" } },
489+
})
490+
local show_body = vim.json.encode({
491+
capabilities = { "completion" },
492+
details = {
493+
family = "custom",
494+
},
495+
})
496+
497+
Curl.get = function(url, opts)
498+
if opts.callback then
499+
opts.callback({ status = 200, body = tags_body })
500+
end
501+
end
502+
Curl.post = function(url, opts)
503+
if opts.callback then
504+
opts.callback({ status = 200, body = show_body })
505+
end
506+
end
507+
508+
local adapter = require("codecompanion.adapters.http").extend("ollama", {
509+
schema = {
510+
model = {
511+
default = "custom:latest",
512+
choices = { "custom:latest" },
513+
},
514+
},
515+
})
516+
517+
return get_models.choices(adapter, { async = false })
518+
]])
519+
520+
h.eq(nil, result["custom:latest"].meta)
521+
h.eq("custom:latest", result["custom:latest"].formatted_name)
522+
end
523+
363524
return T

0 commit comments

Comments
 (0)