From b8689d84af3059f90f29aaf6a0ba296163155e7e Mon Sep 17 00:00:00 2001 From: George Harker Date: Sun, 3 May 2026 11:42:12 +0100 Subject: [PATCH 1/3] feat(adapters): add Kimi (Moonshot) HTTP adapter with thinking support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a dedicated `kimi` adapter for Moonshot's Kimi K2 family. Although Moonshot's API is OpenAI-compatible enough to work via `openai_compatible` for simple chats, the K2-thinking variants impose a strict round-trip requirement that breaks tool-calling chats: When `think` is enabled, every assistant message carrying `tool_calls` must also carry a `reasoning_content` field on replay. Omitting it yields a 400 — `"thinking is enabled but reasoning_content is missing in assistant tool call message at index N"` — on the second turn of any tool-using conversation. OpenAI's Chat Completions schema has no notion of `reasoning_content` on the wire (it nests reasoning behind a `reasoning` object, populated by adapters such as Copilot for gemini-3 or DeepSeek's reasoner), so the generic OpenAI form_messages cannot satisfy Kimi's validator. This adapter wires the round-trip end-to-end: - `chat_output` (delegated to OpenAI) already routes non-standard streaming delta fields through `extra`, so `delta.reasoning_content` chunks land in `extra.reasoning_content` for free. - `parse_message_meta` lifts those fragments onto `data.output.reasoning.content`, the same shape DeepSeek and Copilot use, so CC stores it as `msg.reasoning` on the assistant message. - `form_messages` post-processes OpenAI's output: it rewrites the nested `m.reasoning` into Moonshot's flat `reasoning_content` string on assistant messages, and inserts `reasoning_content = ""` for assistant tool-call messages that have no captured reasoning (chat history that pre-dates the adapter, edited messages, model swaps mid-conversation). The empty-string fallback satisfies the validator without fabricating reasoning content. Other K2 quirks captured in the schema: - `temperature` defaults to `1` (kimi-k2-thinking rejects any other value). - `top_p` defaults to `0.95` (same — pinned by the model). - `think` schema field (boolean, default `true`) so the model is actually asked to reason; the adapter only produces a wire payload Kimi accepts when this is on for k2-thinking variants. Models cover the current K2 line per https://platform.kimi.ai/docs/models — `kimi-k2.6` (default), `kimi-k2.5`, `kimi-k2-thinking{,-turbo}`, `kimi-k2-turbo-preview`, `kimi-k2-{0905,0711}-preview`. Older `moonshot-v1-*` and vision-preview models are intentionally omitted because they don't support tool calling and would need extra setup-time gating. Schema follows the anthropic/openai static-`choices` convention. Structure mirrors `mistral.lua` for review-friendliness: same top-level field order, same handler-key order with `parse_message_meta` slotted between `chat_output` and `tools`, same delegate-to-openai pattern for all standard handlers. The only kimi-specific handler bodies are the two reasoning-handling additions described above. --- lua/codecompanion/adapters/http/kimi.lua | 219 +++++++++++++++++++++++ lua/codecompanion/config.lua | 1 + 2 files changed, 220 insertions(+) create mode 100644 lua/codecompanion/adapters/http/kimi.lua diff --git a/lua/codecompanion/adapters/http/kimi.lua b/lua/codecompanion/adapters/http/kimi.lua new file mode 100644 index 000000000..951270b0f --- /dev/null +++ b/lua/codecompanion/adapters/http/kimi.lua @@ -0,0 +1,219 @@ +local openai = require("codecompanion.adapters.http.openai") + +---@class CodeCompanion.HTTPAdapter.Kimi: CodeCompanion.HTTPAdapter +return { + name = "kimi", + formatted_name = "Kimi", + roles = { + llm = "assistant", + user = "user", + tool = "tool", + }, + opts = { + stream = true, + vision = false, + tools = true, + }, + features = { + text = true, + tokens = true, + }, + url = "${url}${chat_url}", + env = { + url = "https://api.moonshot.ai", + api_key = "MOONSHOT_API_KEY", + chat_url = "/v1/chat/completions", + }, + headers = { + Authorization = "Bearer ${api_key}", + ["Content-Type"] = "application/json", + }, + handlers = { + setup = function(self) + if self.opts and self.opts.stream then + self.parameters.stream = true + self.parameters.stream_options = { include_usage = true } + end + + local model = self.schema.model.default + local model_opts = self.schema.model.choices[model] + if model_opts and model_opts.opts then + self.opts = vim.tbl_deep_extend("force", self.opts, model_opts.opts) + end + + return true + end, + + --- Use the OpenAI adapter for the bulk of the work + tokens = function(self, data) + return openai.handlers.tokens(self, data) + end, + form_tools = function(self, tools) + return openai.handlers.form_tools(self, tools) + end, + form_parameters = function(self, params, messages) + return openai.handlers.form_parameters(self, params, messages) + end, + ---Format the messages for the request. + --- + ---Kimi-k2-thinking rejects assistant messages that contain ``tool_calls`` + ---but no ``reasoning_content`` whenever ``think`` is enabled. We rewrite + ---OpenAI's nested ``reasoning`` field into Moonshot's flat + ---``reasoning_content`` string, and insert an empty-string fallback for + ---tool-call messages whose original reasoning is unavailable (history that + ---pre-dates this adapter, edited messages, model swaps). + ---@param self CodeCompanion.HTTPAdapter + ---@param messages table + ---@return table + form_messages = function(self, messages) + local result = openai.handlers.form_messages(self, messages) + + local think_on = self.parameters and self.parameters.think == true + for _, m in ipairs(result.messages or {}) do + if m.role == self.roles.llm then + if m.reasoning then + m.reasoning_content = type(m.reasoning) == "table" and m.reasoning.content or m.reasoning + m.reasoning = nil + elseif think_on and m.tool_calls then + m.reasoning_content = "" + end + end + end + + return result + end, + chat_output = function(self, data, tools) + return openai.handlers.chat_output(self, data, tools) + end, + ---Lift streamed ``delta.reasoning_content`` onto the message so it can be + ---round-tripped on the next turn (see ``form_messages``). + ---@param self CodeCompanion.HTTPAdapter + ---@param data table + ---@return table + parse_message_meta = function(self, data) + local extra = data.extra + if extra and extra.reasoning_content then + data.output.reasoning = { content = extra.reasoning_content } + if data.output.content == "" then + data.output.content = nil + end + end + return data + end, + tools = { + format_tool_calls = function(self, tools) + return openai.handlers.tools.format_tool_calls(self, tools) + end, + output_response = function(self, tool_call, output) + return openai.handlers.tools.output_response(self, tool_call, output) + end, + }, + inline_output = function(self, data, context) + return openai.handlers.inline_output(self, data, context) + end, + on_exit = function(self, data) + return openai.handlers.on_exit(self, data) + end, + }, + schema = { + ---@type CodeCompanion.Schema + model = { + order = 1, + mapping = "parameters", + type = "enum", + desc = "ID of the Moonshot Kimi model to use. See https://platform.kimi.ai/docs/models.", + default = "kimi-k2.6", + choices = { + -- K2 thinking family (reasoning_content round-trip) + ["kimi-k2-thinking"] = { + formatted_name = "Kimi K2 Thinking", + meta = { context_window = 262144 }, + opts = { can_reason = true }, + }, + ["kimi-k2-thinking-turbo"] = { + formatted_name = "Kimi K2 Thinking Turbo", + meta = { context_window = 262144 }, + opts = { can_reason = true }, + }, + -- K2 general + ["kimi-k2.6"] = { + formatted_name = "Kimi K2.6", + meta = { context_window = 262144 }, + opts = { can_reason = true }, + }, + ["kimi-k2.5"] = { + formatted_name = "Kimi K2.5", + meta = { context_window = 262144 }, + opts = { can_reason = true }, + }, + ["kimi-k2-turbo-preview"] = { + formatted_name = "Kimi K2 Turbo Preview", + meta = { context_window = 262144 }, + }, + ["kimi-k2-0905-preview"] = { + formatted_name = "Kimi K2 0905 Preview", + meta = { context_window = 262144 }, + }, + ["kimi-k2-0711-preview"] = { + formatted_name = "Kimi K2 0711 Preview", + meta = { context_window = 131072 }, + }, + }, + }, + think = { + order = 2, + mapping = "parameters", + type = "boolean", + optional = true, + default = true, + desc = "Enable thinking mode for k2-thinking-class models. When true, the API streams reasoning_content alongside content; this adapter captures and echoes it back on assistant tool-call messages as Moonshot requires.", + }, + temperature = { + order = 3, + mapping = "parameters", + type = "number", + optional = true, + default = 1, + desc = "What sampling temperature to use, between 0 and 2. Note: kimi-k2-thinking only accepts 1.", + validate = function(n) + return n >= 0 and n <= 2, "Must be between 0 and 2" + end, + }, + top_p = { + order = 4, + mapping = "parameters", + type = "number", + optional = true, + default = 0.95, + desc = "Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. Note: kimi-k2-thinking only accepts 0.95.", + validate = function(n) + return n >= 0 and n <= 1, "Must be between 0 and 1" + end, + }, + max_tokens = { + order = 5, + mapping = "parameters", + type = "integer", + optional = true, + default = nil, + desc = "The maximum number of tokens to generate in the completion. The token count of your prompt plus max_tokens cannot exceed the model's context length.", + validate = function(n) + return n > 0, "Must be greater than 0" + end, + }, + stop = { + order = 6, + mapping = "parameters", + type = "list", + optional = true, + default = nil, + subtype = { + type = "string", + }, + desc = "Stop generation if this token is detected. Or if one of these tokens is detected when providing an array.", + validate = function(l) + return #l >= 1, "Must have more than 1 element" + end, + }, + }, +} diff --git a/lua/codecompanion/config.lua b/lua/codecompanion/config.lua index 4ce788836..08c12fa0d 100644 --- a/lua/codecompanion/config.lua +++ b/lua/codecompanion/config.lua @@ -19,6 +19,7 @@ local defaults = { gemini = "gemini", githubmodels = "githubmodels", huggingface = "huggingface", + kimi = "kimi", novita = "novita", mistral = "mistral", ollama = "ollama", From 53a850905b1baa9ee3f74c36eb5e9a4f4809ebd1 Mon Sep 17 00:00:00 2001 From: George Harker Date: Sun, 3 May 2026 12:19:51 +0100 Subject: [PATCH 2/3] tests: cover Kimi (Moonshot) HTTP adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the structure of test_mistral.lua: a top-level adapter set with a pre_case hook that resolves the kimi adapter, then three nested groups — form_messages, Streaming, and No Streaming — using the same hook shape and test phrasing where behaviour overlaps. Standard tests (mirrored from mistral): - form_messages: basic, with tools, and form_tools after extend() - Streaming: chat-buffer output (stream = true pre_case) - No Streaming: chat-buffer output, tools, inline assistant (stream = false pre_case) Kimi-specific additions cover the reasoning_content round-trip the adapter exists for: - form_messages rewrites m.reasoning into a flat reasoning_content string on assistant messages. - When think=true, an empty-string reasoning_content is inserted on assistant tool-call messages with no captured reasoning, satisfying Moonshot's validator on history that pre-dates the adapter. - When think=false, no fallback is inserted (negative case). - Streaming "can process thinking" walks chat_output → parse_message_meta and asserts both content and reasoning aggregate correctly. Stubs follow the OpenAI Chat-Completions wire format (the streaming stub uses delta.reasoning_content; the tools-no-streaming stub includes a flat reasoning_content string on the assistant message). Files added: - tests/adapters/http/test_kimi.lua - tests/adapters/http/stubs/kimi_streaming.txt - tests/adapters/http/stubs/kimi_no_streaming.txt - tests/adapters/http/stubs/kimi_tools_no_streaming.txt --- .../adapters/http/stubs/kimi_no_streaming.txt | 21 ++ tests/adapters/http/stubs/kimi_streaming.txt | 10 + .../http/stubs/kimi_tools_no_streaming.txt | 1 + tests/adapters/http/test_kimi.lua | 287 ++++++++++++++++++ 4 files changed, 319 insertions(+) create mode 100644 tests/adapters/http/stubs/kimi_no_streaming.txt create mode 100644 tests/adapters/http/stubs/kimi_streaming.txt create mode 100644 tests/adapters/http/stubs/kimi_tools_no_streaming.txt create mode 100644 tests/adapters/http/test_kimi.lua diff --git a/tests/adapters/http/stubs/kimi_no_streaming.txt b/tests/adapters/http/stubs/kimi_no_streaming.txt new file mode 100644 index 000000000..9df4f6aea --- /dev/null +++ b/tests/adapters/http/stubs/kimi_no_streaming.txt @@ -0,0 +1,21 @@ +{ + "id": "chatcmpl-kimi-002", + "object": "chat.completion", + "created": 1777802500, + "model": "kimi-k2.6", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Elegant simplicity." + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 3, + "total_tokens": 12 + } +} diff --git a/tests/adapters/http/stubs/kimi_streaming.txt b/tests/adapters/http/stubs/kimi_streaming.txt new file mode 100644 index 000000000..6dd562230 --- /dev/null +++ b/tests/adapters/http/stubs/kimi_streaming.txt @@ -0,0 +1,10 @@ +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"role":"assistant","content":null,"reasoning_content":""},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":null,"reasoning_content":"Two"},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":null,"reasoning_content":" words"},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":null,"reasoning_content":" capturing"},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":null,"reasoning_content":" Ruby"},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":null,"reasoning_content":"."},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":"Elegant","reasoning_content":null},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":" simplicity","reasoning_content":null},"finish_reason":null}],"usage":null} +data: {"id":"chatcmpl-kimi-001","object":"chat.completion.chunk","created":1777802400,"model":"kimi-k2-thinking","choices":[{"index":0,"delta":{"content":".","reasoning_content":null},"finish_reason":"stop"}],"usage":{"prompt_tokens":9,"completion_tokens":7,"total_tokens":16}} +data: [DONE] diff --git a/tests/adapters/http/stubs/kimi_tools_no_streaming.txt b/tests/adapters/http/stubs/kimi_tools_no_streaming.txt new file mode 100644 index 000000000..510b633a9 --- /dev/null +++ b/tests/adapters/http/stubs/kimi_tools_no_streaming.txt @@ -0,0 +1 @@ +{"id":"chatcmpl-kimi-003","object":"chat.completion","created":1777802600,"model":"kimi-k2-thinking","choices":[{"index":0,"finish_reason":"tool_calls","message":{"role":"assistant","content":"","reasoning_content":"Need to fetch weather for both cities.","tool_calls":[{"id":"call_kimi_01","type":"function","function":{"name":"weather","arguments":"{\"location\":\"London, UK\",\"units\":\"celsius\"}"},"index":0},{"id":"call_kimi_02","type":"function","function":{"name":"weather","arguments":"{\"location\":\"Paris, France\",\"units\":\"celsius\"}"},"index":1}]}}],"usage":{"prompt_tokens":420,"completion_tokens":40,"total_tokens":460}} diff --git a/tests/adapters/http/test_kimi.lua b/tests/adapters/http/test_kimi.lua new file mode 100644 index 000000000..9b2a22941 --- /dev/null +++ b/tests/adapters/http/test_kimi.lua @@ -0,0 +1,287 @@ +local h = require("tests.helpers") +local adapter + +local new_set = MiniTest.new_set +T = new_set() + +T["Kimi adapter"] = new_set({ + hooks = { + pre_case = function() + adapter = require("codecompanion.adapters").resolve("kimi") + end, + }, +}) + +T["Kimi adapter"]["form_messages"] = new_set() + +T["Kimi adapter"]["form_messages"]["it can form messages to be sent to the API"] = function() + local messages = { { + content = "Explain Ruby in two words", + role = "user", + } } + + h.eq({ messages = messages }, adapter.handlers.form_messages(adapter, messages)) +end + +T["Kimi adapter"]["form_messages"]["it can form messages with tools"] = function() + local input = { + { role = "system", content = "System Prompt 1" }, + { role = "user", content = "User1" }, + { + role = "llm", + tools = { + calls = { + { + ["function"] = { + arguments = '{"location":"London, UK","units":"fahrenheit"}', + name = "weather", + }, + id = "call_1_a460d461-60a7-468c-a699-ef9e2dced125", + type = "function", + }, + { + ["function"] = { + arguments = '{"location":"Paris, France","units":"fahrenheit"}', + name = "weather", + }, + id = "call_0_bb2a2194-a723-44a6-a1f8-bd05e9829eea", + type = "function", + }, + }, + }, + }, + } + + local expected = { + messages = { + { + content = "System Prompt 1", + role = "system", + }, + { + content = "User1", + role = "user", + }, + { + role = "llm", + tool_calls = { + { + ["function"] = { + arguments = '{"location":"London, UK","units":"fahrenheit"}', + name = "weather", + }, + id = "call_1_a460d461-60a7-468c-a699-ef9e2dced125", + type = "function", + }, + { + ["function"] = { + arguments = '{"location":"Paris, France","units":"fahrenheit"}', + name = "weather", + }, + id = "call_0_bb2a2194-a723-44a6-a1f8-bd05e9829eea", + type = "function", + }, + }, + }, + }, + } + + h.eq(expected, adapter.handlers.form_messages(adapter, input)) +end + +T["Kimi adapter"]["form_messages"]["it can form tools to be sent to the API"] = function() + adapter = require("codecompanion.adapters").extend("kimi", { + schema = { + model = { + default = "kimi-k2.6", + }, + }, + }) + + local weather = require("tests.interactions.chat.tools.builtin.stubs.weather").schema + local tools = { weather = { weather } } + + h.eq({ tools = { weather } }, adapter.handlers.form_tools(adapter, tools)) +end + +T["Kimi adapter"]["form_messages"]["it rewrites m.reasoning to flat reasoning_content on assistant messages"] = function() + -- Role is "assistant" here because CC's chat layer calls map_roles before + -- form_messages, translating its internal LLM_ROLE constant. + local input = { + { role = "user", content = "What is Ruby?" }, + { + role = "assistant", + content = "Elegant simplicity.", + reasoning = "Ruby is a dynamic, object-oriented language...", + }, + } + + local result = adapter.handlers.form_messages(adapter, input) + + h.eq("Ruby is a dynamic, object-oriented language...", result.messages[2].reasoning_content) + h.eq(nil, result.messages[2].reasoning) + h.eq("Elegant simplicity.", result.messages[2].content) +end + +T["Kimi adapter"]["form_messages"]["it inserts empty reasoning_content fallback for tool-call replays when think=true"] = function() + -- Required for k2-thinking on tool-call history that pre-dates this adapter: + -- the validator rejects assistant messages with tool_calls but no reasoning_content. + adapter.parameters = adapter.parameters or {} + adapter.parameters.think = true + + local input = { + { + role = "assistant", + tools = { + calls = { + { + id = "call_abc", + type = "function", + ["function"] = { name = "weather", arguments = '{"location":"London"}' }, + }, + }, + }, + }, + } + + local result = adapter.handlers.form_messages(adapter, input) + h.eq("", result.messages[1].reasoning_content) + h.eq("call_abc", result.messages[1].tool_calls[1].id) +end + +T["Kimi adapter"]["form_messages"]["it does not insert reasoning_content fallback when think=false"] = function() + adapter.parameters = adapter.parameters or {} + adapter.parameters.think = false + + local input = { + { + role = "assistant", + tools = { + calls = { + { + id = "call_abc", + type = "function", + ["function"] = { name = "weather", arguments = "{}" }, + }, + }, + }, + }, + } + + local result = adapter.handlers.form_messages(adapter, input) + h.eq(nil, result.messages[1].reasoning_content) +end + +T["Kimi adapter"]["Streaming"] = new_set({ + hooks = { + pre_case = function() + adapter = require("codecompanion.adapters").extend("kimi", { + opts = { + stream = true, + }, + }) + end, + }, +}) + +T["Kimi adapter"]["Streaming"]["can output streamed data into a format for the chat buffer"] = function() + local lines = vim.fn.readfile("tests/adapters/http/stubs/kimi_streaming.txt") + local output = "" + for _, line in ipairs(lines) do + local chat_output = adapter.handlers.chat_output(adapter, line) + if chat_output and chat_output.output.content then + output = output .. chat_output.output.content + end + end + h.eq("Elegant simplicity.", output) +end + +T["Kimi adapter"]["Streaming"]["can process thinking"] = function() + local content = "" + local reasoning = "" + local lines = vim.fn.readfile("tests/adapters/http/stubs/kimi_streaming.txt") + for _, line in ipairs(lines) do + local chat_output = adapter.handlers.chat_output(adapter, line, {}) + if chat_output and chat_output.extra and adapter.handlers.parse_message_meta then + chat_output = adapter.handlers.parse_message_meta(adapter, chat_output) + end + if chat_output and chat_output.output then + if chat_output.output.content then + content = content .. chat_output.output.content + end + if chat_output.output.reasoning and chat_output.output.reasoning.content then + reasoning = reasoning .. chat_output.output.reasoning.content + end + end + end + + h.eq("Two words capturing Ruby.", reasoning) + h.eq("Elegant simplicity.", content) +end + +-- No streaming --------------------------------------------------------------- + +T["Kimi adapter"]["No Streaming"] = new_set({ + hooks = { + pre_case = function() + adapter = require("codecompanion.adapters").extend("kimi", { + opts = { + stream = false, + }, + }) + end, + }, +}) + +T["Kimi adapter"]["No Streaming"]["can output for the chat buffer"] = function() + local data = vim.fn.readfile("tests/adapters/http/stubs/kimi_no_streaming.txt") + data = table.concat(data, "\n") + + h.eq("Elegant simplicity.", adapter.handlers.chat_output(adapter, data).output.content) +end + +T["Kimi adapter"]["No Streaming"]["can process tools"] = function() + local data = vim.fn.readfile("tests/adapters/http/stubs/kimi_tools_no_streaming.txt") + data = table.concat(data, "\n") + + local tools = {} + + -- Match the format of the actual request + local json = { body = data } + adapter.handlers.chat_output(adapter, json, tools) + + local tool_output = { + { + _index = 1, + ["function"] = { + arguments = '{"location":"London, UK","units":"celsius"}', + name = "weather", + }, + id = "call_kimi_01", + type = "function", + }, + { + _index = 2, + ["function"] = { + arguments = '{"location":"Paris, France","units":"celsius"}', + name = "weather", + }, + id = "call_kimi_02", + type = "function", + }, + } + + h.eq(tool_output, tools) +end + +T["Kimi adapter"]["No Streaming"]["can output for the inline assistant"] = function() + local data = vim.fn.readfile("tests/adapters/http/stubs/kimi_no_streaming.txt") + data = table.concat(data, "\n") + + -- Match the format of the actual request + local json = { body = data } + + h.eq("Elegant simplicity.", adapter.handlers.inline_output(adapter, json).output) +end + +return T From b6db2a98dccc6f949c63086e0b05e2b4ec2a752c Mon Sep 17 00:00:00 2001 From: George Harker Date: Sun, 3 May 2026 12:20:29 +0100 Subject: [PATCH 3/3] docs: document the Kimi (Moonshot) HTTP adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the kimi adapter to the supported-LLMs list (README, doc/index.md, regenerated doc/codecompanion.txt) and a Setup Examples entry in doc/configuration/adapters-http.md. The setup example covers: - Minimal config (just MOONSHOT_API_KEY plus interactions.chat.adapter). - Overriding the API-key source via the cmd: prefix (1Password CLI example) and switching the URL for region-specific endpoints (e.g. api.moonshot.cn). - Schema overrides for `model` and `think` so users can pick a non- thinking K2 model or disable thinking on the K2-thinking variants. - An IMPORTANT callout that kimi-k2-thinking pins temperature=1 and top_p=0.95 server-side, matching the adapter's defaults. The example is placed between the llama.cpp and Ollama sections — both neighbours involve OpenAI-compatible reasoning configuration, which keeps the page topically grouped. --- README.md | 2 +- doc/codecompanion.txt | 56 +++++++++++++++++++++++++++++- doc/configuration/adapters-http.md | 45 ++++++++++++++++++++++++ doc/index.md | 1 + 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85ea4490c..2296bc949 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Thank you to the following people: - :speech_balloon: [Copilot Chat](https://github.com/features/copilot) meets [Zed AI](https://zed.dev/blog/zed-ai), in Neovim - :zap: Integrates Neovim with LLMs and Agents in the CLI -- :electric_plug: Support for LLMs from Anthropic, Copilot, GitHub Models, DeepSeek, Gemini, Mistral AI, Novita, Ollama, OpenAI, Azure OpenAI, HuggingFace and xAI (or [bring your own](https://codecompanion.olimorris.dev/extending/adapters.html)) +- :electric_plug: Support for LLMs from Anthropic, Copilot, GitHub Models, DeepSeek, Gemini, Kimi (Moonshot), Mistral AI, Novita, Ollama, OpenAI, Azure OpenAI, HuggingFace and xAI (or [bring your own](https://codecompanion.olimorris.dev/extending/adapters.html)) - :robot: Support for [Agent Client Protocol](https://agentclientprotocol.com/overview/introduction), enabling coding with agents like [Augment Code](https://docs.augmentcode.com/cli/overview), [Cagent](https://github.com/docker/cagent) from Docker, [Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview), [Codex](https://openai.com/codex), [Copilot CLI](https://github.com/features/copilot/cli), [Gemini CLI](https://github.com/google-gemini/gemini-cli), [Goose](https://block.github.io/goose/), [Cursor CLI](https://cursor.com/docs/cli/overview), [Kimi CLI](https://github.com/MoonshotAI/kimi-cli), [Kiro](https://kiro.dev/docs/cli/), [Mistral Vibe](https://github.com/mistralai/mistral-vibe) and [OpenCode](https://opencode.ai) - :heart_hands: User contributed and supported [adapters](https://codecompanion.olimorris.dev/configuration/adapters-http#community-adapters) - :battery: Support for [Model Context Protocol (MCP)](https://codecompanion.olimorris.dev/model-context-protocol#model-context-protocol-mcp-support) diff --git a/doc/codecompanion.txt b/doc/codecompanion.txt index 601f0e5ac..b478feb5f 100644 --- a/doc/codecompanion.txt +++ b/doc/codecompanion.txt @@ -1,4 +1,4 @@ -*codecompanion.txt* For NVIM v0.11 Last change: 2026 April 29 +*codecompanion.txt* For NVIM v0.11 Last change: 2026 May 03 ============================================================================== Table of Contents *codecompanion-table-of-contents* @@ -126,6 +126,7 @@ agents. Out of the box, the plugin supports: - Goose (`goose`) - Requires an API key - HuggingFace (`huggingface`) - Requires an API key - Kilo Code (`kilocode`) - Requires an API key +- Kimi (`kimi`) - Moonshot's Kimi K2 family; requires an API key - Kimi CLI (`kimi_cli`) - Requires an API key - Mistral AI (`mistral`) - Requires an API key or a Le Chat Pro subscription - Novita (`novita`) - Requires an API key @@ -1757,6 +1758,59 @@ LLAMA.CPP WITH --REASONING-FORMAT DEEPSEEK < +KIMI (MOONSHOT) + +CodeCompanion ships a built-in `kimi` adapter for Moonshot's Kimi K2 family +. Unlike the generic `openai_compatible` +adapter, it captures and round-trips Kimi's `reasoning_content` so the +K2-thinking variants (`kimi-k2-thinking`, `kimi-k2-thinking-turbo`, and the +`can_reason` K2 generals such as `kimi-k2.6`) work correctly with tool calling +— without it, the second turn of a tool-using chat fails with `"thinking is +enabled but reasoning_content is missing in assistant tool call message"`. + +For the default setup, simply set `MOONSHOT_API_KEY` and pick the adapter: + +>lua + require("codecompanion").setup({ + interactions = { + chat = { adapter = "kimi" }, + inline = { adapter = "kimi" }, + }, + }) +< + +To override the API-key source, swap models, or disable thinking mode: + +>lua + require("codecompanion").setup({ + adapters = { + http = { + kimi = function() + return require("codecompanion.adapters").extend("kimi", { + env = { + -- Use the 1Password CLI instead of an environment variable: + api_key = "cmd:op read op://API/Kimi/credential --no-newline", + -- Region override (Moonshot has separate endpoints, e.g. for China): + -- url = "https://api.moonshot.cn", + }, + schema = { + model = { default = "kimi-k2.6" }, + -- Set to false to disable thinking mode (e.g. for the K2-general + -- non-reasoning preview models, where `think` is a no-op): + think = { default = true }, + }, + }) + end, + }, + }, + }) +< + + + [!IMPORTANT] The K2-thinking models pin `temperature` to `1` and `top_p` to + `0.95`; the adapter's defaults match. Overriding either with another value will + yield a 400 from the API. Other K2 models accept the full ranges. + OLLAMA (REMOTELY) The simplest way to connect to a remote Ollama instance is to set the diff --git a/doc/configuration/adapters-http.md b/doc/configuration/adapters-http.md index a30b490b6..1e94cba7b 100644 --- a/doc/configuration/adapters-http.md +++ b/doc/configuration/adapters-http.md @@ -380,6 +380,51 @@ require("codecompanion").setup({ }) ``` +### Kimi (Moonshot) + +CodeCompanion ships a built-in `kimi` adapter for Moonshot's [Kimi K2 family](https://platform.kimi.ai/docs/models). Unlike the generic `openai_compatible` adapter, it captures and round-trips Kimi's `reasoning_content` so the K2-thinking variants (`kimi-k2-thinking`, `kimi-k2-thinking-turbo`, and the `can_reason` K2 generals such as `kimi-k2.6`) work correctly with tool calling — without it, the second turn of a tool-using chat fails with `"thinking is enabled but reasoning_content is missing in assistant tool call message"`. + +For the default setup, simply set `MOONSHOT_API_KEY` and pick the adapter: + +```lua +require("codecompanion").setup({ + interactions = { + chat = { adapter = "kimi" }, + inline = { adapter = "kimi" }, + }, +}) +``` + +To override the API-key source, swap models, or disable thinking mode: + +```lua +require("codecompanion").setup({ + adapters = { + http = { + kimi = function() + return require("codecompanion.adapters").extend("kimi", { + env = { + -- Use the 1Password CLI instead of an environment variable: + api_key = "cmd:op read op://API/Kimi/credential --no-newline", + -- Region override (Moonshot has separate endpoints, e.g. for China): + -- url = "https://api.moonshot.cn", + }, + schema = { + model = { default = "kimi-k2.6" }, + -- Set to false to disable thinking mode (e.g. for the K2-general + -- non-reasoning preview models, where `think` is a no-op): + think = { default = true }, + }, + }) + end, + }, + }, +}) +``` + +> [!IMPORTANT] +> The K2-thinking models pin `temperature` to `1` and `top_p` to `0.95`; the adapter's defaults match. Overriding either with another value will yield a 400 from the API. Other K2 models accept the full ranges. + ### Ollama (remotely) The simplest way to connect to a remote Ollama instance is to set the `OLLAMA_HOST` environment variable (the same variable used by the Ollama CLI): diff --git a/doc/index.md b/doc/index.md index 4b454e4ed..11f1d09c4 100644 --- a/doc/index.md +++ b/doc/index.md @@ -57,6 +57,7 @@ CodeCompanion uses [HTTP](configuration/adapters-http) and [ACP](configuration/a - Goose (`goose`) - Requires an API key - HuggingFace (`huggingface`) - Requires an API key - Kilo Code (`kilocode`) - Requires an API key +- Kimi (`kimi`) - Moonshot's Kimi K2 family; requires an API key - Kimi CLI (`kimi_cli`) - Requires an API key - Mistral AI (`mistral`) - Requires an API key or a Le Chat Pro subscription - Novita (`novita`) - Requires an API key