Neovim: connecting avante.nvim and codecompanion
avante.nvim and codecompanion.nvim handle addresses differently under the hood — one provider expects /v1, the other doesn't. Both configs, line by line.
4 min · · Updated · IDE · терминал
avante.nvim and codecompanion.nvim handle providers differently under the hood, and that directly affects what goes in the address field. avante's claude provider is built for the Anthropic-compatible protocol and appends /v1/messages itself — the base address needs no suffix. avante's openai provider, by contrast, expects the /v1 prefix already baked into the address, while codecompanion's openai_compatible adapter appends /v1/chat/completions itself — it needs the address without the suffix. Mixing these cases up is the fastest way to get a 404 right after installing.
An environment variable for the key
Both plugins read the key from an environment variable rather than storing it as plain text in the config. In .zshrc or .bashrc:
export FETCH_API_KEY="fetch_your_key"The Lua config then pulls it via os.getenv("FETCH_API_KEY") or vim.env.FETCH_API_KEY.
avante.nvim: the claude provider
require("avante").setup({
provider = "claude",
providers = {
claude = {
endpoint = "https://api.zukko.pro",
model = "claude-sonnet-5",
api_key_name = "FETCH_API_KEY",
extra_request_body = {
max_tokens = 4096,
},
},
},
})Note the endpoint has no /v1. avante's claude provider hits /v1/messages on its own and adds the anthropic-version: 2023-06-01 header itself. Adding /v1 here out of habit sends every request to .../v1/v1/messages, which returns 404.
avante.nvim: the openai provider (for a non-Claude model)
require("avante").setup({
provider = "openai",
providers = {
openai = {
endpoint = "https://api.zukko.pro/v1",
model = "gpt-5.6-sol",
api_key_name = "FETCH_API_KEY",
},
},
})Here it's the opposite — /v1 in the address is required, because the openai provider hits /chat/completions directly with no path-building logic of its own.
codecompanion.nvim: the openai_compatible adapter
require("codecompanion").setup({
adapters = {
zukko = function()
return require("codecompanion.adapters").extend("openai_compatible", {
env = {
url = "https://api.zukko.pro",
api_key = "FETCH_API_KEY",
},
schema = {
model = {
default = "glm-5.3",
},
},
})
end,
},
strategies = {
chat = { adapter = "zukko" },
inline = { adapter = "zukko" },
},
})codecompanion's openai_compatible adapter is generic across all OpenAI-compatible backends and appends /v1/chat/completions to url itself, so url goes in without /v1. Add the suffix and requests land on .../v1/v1/chat/completions and come back 404.
Test the key before touching the Lua config
If a plugin stays silent or fails with a cryptic error after setup, it's faster to test the key directly first:
curl https://api.zukko.pro/v1/messages \
-H "Authorization: Bearer $FETCH_API_KEY" \
-H "Content-Type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-sonnet-5","max_tokens":64,"messages":[{"role":"user","content":"ping"}]}'If curl answers and the plugin doesn't, the bug is in the Lua config, not the key or balance.
Which model for which editor task
For getting oriented in an unfamiliar repository, claude-opus-5-5 fits well — the 1,000,000-token context of the Claude and GPT models (GLM has 128K) lets you drop dozens of files into one request, and Opus 5.5's extended thinking pays off specifically on architecture-level questions, not line-by-line edits. For routine work like "add a type to this function" or inline completion inside codecompanion, glm-5.3-flash makes more sense — noticeably cheaper ($0.04 versus $0.3 per million tokens); whether the quality holds up on short, targeted requests is worth checking on your own tasks.
Caching repeated requests on unchanged code
If you run the same prompt against the same chunk of code several times in a row (rerunning docstring generation without changing either the file or the instruction, say), identical requests are cached gateway-side and come back instantly and free — the response carries X-Zukko-Cache: hit. The primary way to turn the cache on is the X-Zukko-Cache: true header, which works for any model; temperature: 0 also enables it, but only for models that accept that value (newer Claude models — claude-sonnet-5, claude-opus-5-5 — may reject a temperature other than 1 with a 400). The cache only hits on an exactly identical request body: one changed word in the instruction or line in the file makes it a new request. Cache hits are not written to /v1/usage. Neither avante nor codecompanion surfaces that header in the UI, but if you're maintaining your own thin wrapper and logging the raw response, it's easy to check.
Idempotency across a dropped SSH session
If Neovim runs over SSH and the session drops mid-request, resending the same prompt is routine. An Idempotency-Key header with a unique value per logical request guards against double charging: retrying with the same idempotency key within 24 hours returns the same response if the original request actually completed server-side. Neither plugin generates this header out of the box — adding it only works if you write your own thin wrapper around the API instead of using the stock adapter.
A separate key for the work machine
It's worth giving Neovim its own key with a daily spending limit and a restricted model list, both configurable on that key's page in the dashboard. If a config accidentally leaves an autocomplete loop running, or a batch-refactor script fires across the whole project, the limit stops the spend before it shows up at month's end.
If a model is temporarily disabled
A 503 provider_unavailable or model_disabled response means one specific model is switched off in the catalogue for a while — neither avante nor codecompanion tells this apart from other network errors and will just show a generic failure in the chat window. The fastest fix is to temporarily swap the model value in the Lua config for a neighboring model from the same family and reload the plugin config (:so % or restarting Neovim), rather than spending time digging through logs for a network problem that isn't there.
The two protocols return different response shapes
Another source of confusion when debugging with curl: /v1/messages returns an Anthropic-shaped body (a content field holding an array of blocks), while /v1/chat/completions returns an OpenAI-shaped one (a choices field with a nested message.content). If you write your own response parser on top of a stock adapter — to strip out everything but the text, say — these two shapes aren't interchangeable, and code written for one will fail on the other trying to read a field that doesn't exist.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- JetBrains AI Assistant not working: causes and workaroundsAI Assistant will not activate, stays silent or is off for some files. We go through the causes in the JetBrains docs and connect your own models via a gateway.
- DeepSeek in VS Code: connecting through Cline, Kilo Code and ContinueWe connect DeepSeek to Cline, Kilo Code and Continue with a single key, choose between Flash and Pro, and estimate the cost of a working day with an agent in the editor.
- GitHub Copilot alternatives with your own API key: what to chooseCline, Kilo Code, Continue and Cursor work with your own key. We compare where they live, how they connect to a gateway and how to estimate spend.
- Invalid API key in opencode: why the key is rejected and how to checkThe Invalid API key error in opencode almost always comes down to four causes. We test the key outside opencode and find where the chain breaks.