diff --git a/common/nvim/init.lua b/common/nvim/init.lua index 0ef0847..87db4b7 100755 --- a/common/nvim/init.lua +++ b/common/nvim/init.lua @@ -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 = "", - accept_line = "", - prev = "", - next = "", - dismiss = "", - }, - }, - 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 = "", + accept_line = "", + prev = "", + next = "", + dismiss = "", + }, + }, + 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