nvim: fix two real bugs in the minuet config, both of which loaded cleanly
⚠ 1. The whole require() was guarded behind "is the key present", so with no key the module never loaded, `:Minuet` was never registered, and nvim gave NO indication why. That is exactly the failure it hit in practice. `:Minuet` is created at module load, not inside setup(), and the plugin already notifies clearly when the key is missing -- let it do that job. setup() now always runs, and `:MinuetStatus` reports endpoint, model and whether a key resolved. ⚠ 2. The FIM template took `(ctx)` and read `ctx.lines_before`. minuet calls it as `template.prompt(before, after, opts)` -- three positional args -- so the first real completion died with "attempt to concatenate field 'lines_after' (a nil value)". The config loaded perfectly cleanly the whole time. A config that loads is not a config that works, and only a functional test found it. The key now resolves lazily and is cached: env var first, then rbw with the host-appropriate entry (litellm-key-work on sctfw004, where that key IS the policy boundary). `false` is cached for "looked and found nothing" so a locked rbw costs one probe per session, not one per keystroke. Verified against the live path: provider reports available, the key resolves, and /v1/completions returns correct FIM infills both streaming and not.
This commit is contained in:
1 parent
07149fe54a
commit
4cc6783438
1 file changed
+113
-55
+113
-55
@@ -120,67 +120,125 @@ vim.lsp.config("rust_analyzer", {
|
||||
-- ttl, so if another model was used in between, the next completion pays a
|
||||
-- full reload. Measured: ~10 s cold, 0.7-1.3 s warm. Accepted deliberately --
|
||||
-- concurrent agentic work is not viable on this hardware anyway.
|
||||
-- ⚠ ALWAYS call setup(). The first version of this guarded the whole require()
|
||||
-- behind "is the key present", which meant that with no key the module never
|
||||
-- loaded, `:Minuet` was never registered, and nvim gave NO indication why --
|
||||
-- the exact failure it hit in practice. `:Minuet` is created at module load,
|
||||
-- not inside setup(), and the plugin already notifies clearly when the key is
|
||||
-- missing. Let it do that job instead of silently doing nothing.
|
||||
--
|
||||
-- The key resolves lazily and is cached: env var first, then rbw, matching the
|
||||
-- opencode convention of reading keys at launch rather than writing them to
|
||||
-- disk. `false` is cached for "looked and found nothing" so a locked rbw costs
|
||||
-- one probe per session rather than one per keystroke.
|
||||
local _minuet_key
|
||||
local function minuet_key()
|
||||
-- Set in ~/.secrets.zsh (untracked, mode 600) -- never in this repo.
|
||||
-- The work laptop must point this at `litellm-key-work`, whose virtual key
|
||||
-- is what enforces the no-Anthropic/no-PRC boundary server-side.
|
||||
local k = os.getenv("LITELLM_KEY_NVIM")
|
||||
if k and k ~= "" then return k end
|
||||
if _minuet_key ~= nil then
|
||||
return _minuet_key or nil
|
||||
end
|
||||
local k = vim.env.LITELLM_KEY_NVIM
|
||||
if k and k ~= "" then
|
||||
_minuet_key = k
|
||||
return k
|
||||
end
|
||||
if vim.fn.executable("rbw") == 1 then
|
||||
vim.fn.system({ "rbw", "unlocked" })
|
||||
if vim.v.shell_error == 0 then
|
||||
-- ⚠ The work laptop MUST use the work key: it is enforced
|
||||
-- server-side to 403 Anthropic and PRC-origin models, and a
|
||||
-- personal key here would silently remove that guarantee for
|
||||
-- everything nvim sends.
|
||||
local host = vim.uv.os_gethostname() or ""
|
||||
local entry = host:match("sctfw004") and "litellm-key-work"
|
||||
or "litellm-key-personal"
|
||||
local out = vim.fn.system({ "rbw", "get", entry })
|
||||
if vim.v.shell_error == 0 and vim.trim(out) ~= "" then
|
||||
_minuet_key = vim.trim(out)
|
||||
return _minuet_key
|
||||
end
|
||||
end
|
||||
end
|
||||
_minuet_key = false
|
||||
return nil
|
||||
end
|
||||
|
||||
if minuet_key() then
|
||||
local base = os.getenv("LITELLM_BASE")
|
||||
if not base or base == "" then base = "https://llm.rcjohnstone.com" end
|
||||
|
||||
require("minuet").setup({
|
||||
provider = "openai_fim_compatible",
|
||||
-- Ghost text, because there is no completion engine installed here
|
||||
-- (no nvim-cmp, no blink) and virtual text is the copilot-shaped one.
|
||||
virtualtext = {
|
||||
auto_trigger_ft = { "python", "rust", "lua", "c", "cpp", "julia", "sh" },
|
||||
keymap = {
|
||||
accept = "<A-a>",
|
||||
accept_line = "<A-l>",
|
||||
prev = "<A-[>",
|
||||
next = "<A-]>",
|
||||
dismiss = "<A-e>",
|
||||
},
|
||||
},
|
||||
provider_options = {
|
||||
openai_fim_compatible = {
|
||||
api_key = "LITELLM_KEY_NVIM",
|
||||
name = "litellm",
|
||||
end_point = base .. "/v1/completions",
|
||||
model = "qwen3-coder-30b",
|
||||
-- ⚠ KEEP THIS SMALL. Measured 2026-09-20 on the real path: at
|
||||
-- max_tokens 48 the model happily continued past the useful
|
||||
-- completion into whole extra functions -- `def add` came back
|
||||
-- with `sub` and `mul` attached. Most of the latency was spent
|
||||
-- generating text that would be thrown away. Raise it only if
|
||||
-- completions are being visibly truncated mid-expression.
|
||||
optional = {
|
||||
max_tokens = 32,
|
||||
top_p = 0.9,
|
||||
stop = { "<|fim_pad|>", "<|endoftext|>", "\n\n" },
|
||||
},
|
||||
template = {
|
||||
prompt = function(ctx)
|
||||
return "<|fim_prefix|>" .. ctx.lines_before
|
||||
.. "<|fim_suffix|>" .. ctx.lines_after
|
||||
.. "<|fim_middle|>"
|
||||
end,
|
||||
suffix = false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
else
|
||||
-- Silent by design: the NAS is headless and never needs this, and a
|
||||
-- startup error on every nvim launch is how a warning stops being read.
|
||||
vim.g.minuet_disabled_reason = "LITELLM_KEY_NVIM unset; see common/zsh/secrets.zsh.example"
|
||||
local minuet_base = vim.env.LITELLM_BASE
|
||||
if not minuet_base or minuet_base == "" then
|
||||
minuet_base = "https://llm.rcjohnstone.com"
|
||||
end
|
||||
|
||||
require("minuet").setup({
|
||||
provider = "openai_fim_compatible",
|
||||
-- Default is 'warn', which hides request failures. If completions stop
|
||||
-- appearing, set this to 'debug' and the reason shows up in `:messages`.
|
||||
notify = "warn",
|
||||
-- Ghost text: there is no completion engine here (no nvim-cmp, no blink)
|
||||
-- and virtual text is the copilot-shaped one.
|
||||
virtualtext = {
|
||||
auto_trigger_ft = { "python", "rust", "lua", "c", "cpp", "julia", "sh" },
|
||||
keymap = {
|
||||
accept = "<A-a>",
|
||||
accept_line = "<A-l>",
|
||||
prev = "<A-[>",
|
||||
next = "<A-]>",
|
||||
dismiss = "<A-e>",
|
||||
},
|
||||
},
|
||||
provider_options = {
|
||||
openai_fim_compatible = {
|
||||
-- A FUNCTION, not an env var name: this is what lets the key come
|
||||
-- from rbw when the shell did not export one.
|
||||
api_key = minuet_key,
|
||||
name = "litellm",
|
||||
end_point = minuet_base .. "/v1/completions",
|
||||
model = "qwen3-coder-30b",
|
||||
-- ⚠ KEEP THIS SMALL. Measured on the real path at 48: `def add`
|
||||
-- came back with `sub` and `mul` attached, so most of the latency
|
||||
-- was spent generating text that would be discarded. Raise it only
|
||||
-- if completions are being truncated mid-expression.
|
||||
optional = {
|
||||
max_tokens = 32,
|
||||
top_p = 0.9,
|
||||
stop = { "<|fim_pad|>", "<|endoftext|>", "\n\n" },
|
||||
},
|
||||
-- ⚠ THREE POSITIONAL ARGS, not a context table. minuet calls
|
||||
-- this as `template.prompt(before, after, opts)` -- the first
|
||||
-- version took `(ctx)` and died with "attempt to concatenate
|
||||
-- field 'lines_after' (a nil value)" on the first real
|
||||
-- completion, while loading perfectly cleanly. A config that
|
||||
-- loads is not a config that works.
|
||||
--
|
||||
-- `suffix = false` because the whole FIM prompt is built here;
|
||||
-- letting minuet also send a `suffix` field would give the server
|
||||
-- two competing descriptions of the same gap.
|
||||
template = {
|
||||
prompt = function(before, after, _opts)
|
||||
return "<|fim_prefix|>" .. (before or "")
|
||||
.. "<|fim_suffix|>" .. (after or "")
|
||||
.. "<|fim_middle|>"
|
||||
end,
|
||||
suffix = false,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
-- Because "is it on, and if not why" should be answerable without reading Lua.
|
||||
vim.api.nvim_create_user_command("MinuetStatus", function()
|
||||
local k = minuet_key()
|
||||
local lines = {
|
||||
"endpoint: " .. minuet_base .. "/v1/completions",
|
||||
"model: qwen3-coder-30b",
|
||||
"key: " .. (k and ("found (" .. #k .. " chars)") or "NOT FOUND"),
|
||||
}
|
||||
if not k then
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Set LITELLM_KEY_NVIM in ~/.secrets.zsh, or unlock rbw")
|
||||
table.insert(lines, "(`rbw unlock`) so it can be read from there.")
|
||||
end
|
||||
vim.notify(table.concat(lines, "\n"), k and vim.log.levels.INFO or vim.log.levels.WARN)
|
||||
end, { desc = "Show minuet FIM autocomplete status" })
|
||||
|
||||
vim.cmd("colorscheme vague")
|
||||
|
||||
-- KEYMAPS
|
||||
|
||||
Reference in new issue
Block a user