A Neovim plugin for integrating Large Language Models (LLMs) into your coding workflow.
kznllm_demo_v2.mp4
The only main command is leader + k, it does nothing more than fill in some LLM completion into the text buffer. It has two main behaviors:
- If you made a visual selection, it will attempt to replace your selection with a valid code fragment.
- If you make no visual selection, it can yap freely (or do something else specified by a good template).
Note
project-mode is also available when you have a directory named .kzn. It will use the folder closest to your current working directory and traverse backwards until it finds a .kzn directory or reaches your home directory and exits.
It's easy to hack on and implement customize behaviors without understanding much about nvim plugins. Try the default preset configuration provided below, but I recommend you fork the repo and using the preset as a reference for implementing your own features.
- close-to-natty coding experience
- add custom prompt templates
- pipe any context into template engine
- extend with custom features/modes
Note
This plugin depends on minijinja-cli - way easier to compose prompts.
- Install
minijinja-cli(required for prompt templating):
cargo install minijinja-cli2.1 Add the plugin to your Neovim configuration using Lazy.nvim:
{
'chottolabs/kznllm.nvim',
dependencies = { 'nvim-lua/plenary.nvim' },
config = function()
-- Add your configuration here (see Configuration section below)
end
}2.2 Or, add the plugin to your Neovim configuration using plug.vim:
Plug 'nvim-lua/plenary.nvim'
Plug 'chottolabs/kznllm.nvim'Then, in your init.vim or init.lua, add the following configuration:
require('kznllm').setup({
-- Add your configuration here (see Configuration section below)
})Make your API keys available via environment variables
export LAMBDA_API_KEY=secret_...
export ANTHROPIC_API_KEY=sk-...
export OPENAI_API_KEY=sk-proj-...
export GROQ_API_KEY=gsk_...
export DEEPSEEK_API_KEY=vllm_...
export VLLM_API_KEY=vllm_...
Full config with a preset switcher mechanism and optional debugging:
{
'chottolabs/kznllm.nvim',
-- dev = true,
-- dir = /path/to/your/fork,
dependencies = {
{ 'nvim-lua/plenary.nvim' }
},
config = function(self)
local presets = require 'kznllm.presets'
-- bind a key to the preset switcher
vim.keymap.set({ 'n', 'v' }, '<leader>m', presets.switch_presets, { desc = 'switch between presets' })
local function llm_fill()
local selected_preset = presets.load()
presets.invoke_llm(selected_preset)
end
vim.keymap.set({ 'n', 'v' }, '<leader>k', llm_fill, { desc = 'Send current selection to LLM llm_fill' })
-- optional for debugging purposes
local function debug()
local selected_preset = presets.load()
presets.invoke_llm(selected_preset, { debug = true })
end
vim.keymap.set({ 'n', 'v' }, '<leader>d', debug, { desc = 'Send current selection to LLM debug' })
vim.api.nvim_set_keymap('n', '<Esc>', '', {
noremap = true,
silent = true,
callback = function()
vim.api.nvim_exec_autocmds('User', { pattern = 'LLM_Escape' })
end,
})
end
},Originally based on dingllm.nvim - but diverged quite a bit
- prompts user for additional context before filling
- structured to make the inherent coupling between neovim logic, LLM streaming spec, and model-specific templates more explicit
- uses jinja as templating engine for ensuring correctness in more complex prompts
- preset defaults + simple approach for overriding them
- free cursor movement during generation
- avoids "undojoin after undo" error
Preset switcher with added presets
local extra_presets = {
{
id = 'r1-qwen-32B',
provider = 'huggingface',
spec = 'openai',
opts = {
model = 'deepseek-ai/DeepSeek-R1-Distill-Qwen-32B',
data_params = {
max_tokens = 8192,
temperature = 0.3,
},
api_key_name = 'HUGGINGFACE_API_KEY',
base_url = 'https://api-inference.huggingface.co',
endpoint = '/v1/chat/completions',
},
},
}
local presets = require 'kznllm.presets'
local kznllm = require 'kznllm'
presets.register_presets(extra_presets)
vim.keymap.set({ 'n', 'v' }, '<leader>m', presets.switch_presets, { desc = 'switch between presets' })
local function llm_fill()
local selected_preset = presets.load(all_presets)
presets.invoke_llm(selected_preset)
end
vim.keymap.set({ 'n', 'v' }, '\\', llm_fill, { desc = 'Send current selection to LLM llm_fill' })
local function debug()
local selected_preset = presets.load(all_presets)
presets.invoke_llm(selected_preset, { debug = true })
end
vim.keymap.set({ 'n', 'v' }, '<leader>\\', debug, { desc = 'Send current selection to LLM debug' })Minimal configuration with no preset switcher and a custom template directory
local Path = require 'plenary.path'
local TEMPLATE_DIRECTORY = Path:new(vim.fn.expand('~') .. '/templates')
local function llm_fill()
presets.invoke_llm({
id = 'r1-llama-70B-ln-or',
-- prompt = 'ask claude' -- optional. set an alternative input prompt
spec = 'openai', -- required. 'openai' | 'anthropic' | 'lndiff/openai' | 'lndiff/anthropic'
opts = {
model = 'deepseek/deepseek-r1-distill-llama-70b',
data_params = {
max_tokens = 8192,
temperature = 0.7,
},
api_key_name = 'OPENROUTER_API_KEY', -- optional
base_url = 'https://openrouter.ai/api', -- optional
-- endpoint = '/v1/chat/completions', -- optional
-- template_directory = TEMPLATE_DIRECTORY, -- optional. set an alternative template directory
-- template_scope = 'openrouter', -- optional. set an alternative template scope (template will be searched in `template_directory/template_scope/..` )
}
})
end
vim.keymap.set({ 'n', 'v' }, '<leader>f', llm_fill, { desc = 'Send current selection to LLM llm_fill' })Minimal VLLM configuration with no preset switcher
local function llm_fill()
presets.invoke_llm({
id = 'qwen-2.5-1.5b-vllm',
spec = 'vllm',
opts = {
model = 'Qwen/Qwen2.5-1.5B-Instruct',
data_params = {
max_tokens = 512,
temperature = 0.7,
},
api_key_name = 'VLLM_API_KEY',
base_url = 'http://localhost:8000/v1'
}
})
end
vim.keymap.set({ 'n', 'v' }, '<leader>f', llm_fill, { desc = 'Send current selection to VLLM' })