Skip to content

Commit 9cc2933

Browse files
authored
feat(adapters): support extra fields in OpenAI adapter (#2359)
1 parent 32dd2da commit 9cc2933

File tree

8 files changed

+322
-79
lines changed

8 files changed

+322
-79
lines changed

doc/codecompanion.txt

Lines changed: 140 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*codecompanion.txt* For NVIM v0.11 Last change: 2025 November 17
1+
*codecompanion.txt* For NVIM v0.11 Last change: 2025 November 19
22

33
==============================================================================
44
Table of Contents *codecompanion-table-of-contents*
@@ -643,7 +643,7 @@ CodeCompanion, you simply need to follow their Getting Started
643643
<https://docs.augmentcode.com/cli/overview#getting-started> guide.
644644

645645

646-
SETUP: DOCKER CAGENT ~
646+
SETUP: CAGENT ~
647647

648648
To use Docker’s Cagent <https://github.com/docker/cagent> within
649649
CodeCompanion, you need to follow these steps:
@@ -878,9 +878,10 @@ The configuration for both types of adapters is exactly the same, however they
878878
sit within their own tables (`adapters.http.*` and `adapters.acp.*`) and have
879879
different options available. HTTP adapters use `models` to allow users to
880880
select the specific LLM they’d like to interact with. ACP adapters use
881-
`commands` to allow users to customize their interaction with agents (e.g.�
882-
enabling `yolo` mode). As there is a lot of shared functionality between the
883-
two adapters, it is recommend that you read this page alongside the ACP one.
881+
`commands` to allow users to customize their interaction with agents
882+
(e.g. enabling `yolo` mode). As there is a lot of shared functionality between
883+
the two adapters, it is recommend that you read this page alongside the ACP
884+
one.
884885

885886

886887
CHANGING THE DEFAULT ADAPTER ~
@@ -912,7 +913,7 @@ the adapter’s URL, headers, parameters and other fields at runtime.
912913

913914
Supported `env` value types: - **Plain environment variable name (string)**: if
914915
the value is the name of an environment variable that has already been set
915-
(e.g.`"HOME"` or `"GEMINI_API_KEY"`), the plugin will read the value. -
916+
(e.g. `"HOME"` or `"GEMINI_API_KEY"`), the plugin will read the value. -
916917
**Command (string prefixed with cmd:)**: any value that starts with `cmd:` will
917918
be executed via the shell. Example: `"cmd:op read
918919
op://personal/Gemini/credential --no-newline"`. - **Function**: you can provide
@@ -1287,6 +1288,88 @@ the plugin:
12871288
<
12881289

12891290

1291+
SETUP: OPENROUTER WITH REASONING OUTPUT ~
1292+
1293+
>lua
1294+
require("codecompanion").setup({
1295+
adapters = {
1296+
http = {
1297+
openrouter = function()
1298+
return require("codecompanion.adapters").extend("openai_compatible", {
1299+
env = {
1300+
url = "https://openrouter.ai/api",
1301+
api_key = "OPENROUTER_API_KEY",
1302+
chat_url = "/v1/chat/completions",
1303+
},
1304+
handlers = {
1305+
parse_message_meta = function(self, data)
1306+
local extra = data.extra
1307+
if extra and extra.reasoning then
1308+
data.output.reasoning = { content = extra.reasoning }
1309+
if data.output.content == "" then
1310+
data.output.content = nil
1311+
end
1312+
end
1313+
return data
1314+
end,
1315+
},
1316+
})
1317+
end,
1318+
},
1319+
},
1320+
strategies = {
1321+
chat = {
1322+
adapter = "openrouter",
1323+
},
1324+
inline = {
1325+
adapter = "openrouter",
1326+
},
1327+
},
1328+
})
1329+
<
1330+
1331+
1332+
SETUP: LLAMA.CPP WITH --REASONING-FORMAT DEEPSEEK ~
1333+
1334+
>lua
1335+
require("codecompanion").setup({
1336+
adapters = {
1337+
http = {
1338+
["llama.cpp"] = function()
1339+
return require("codecompanion.adapters").extend("openai_compatible", {
1340+
env = {
1341+
url = "http://127.0.0.1:8080", -- replace with your llama.cpp instance
1342+
api_key = "TERM",
1343+
chat_url = "/v1/chat/completions",
1344+
},
1345+
handlers = {
1346+
parse_message_meta = function(self, data)
1347+
local extra = data.extra
1348+
if extra and extra.reasoning_content then
1349+
data.output.reasoning = { content = extra.reasoning_content }
1350+
if data.output.content == "" then
1351+
data.output.content = nil
1352+
end
1353+
end
1354+
return data
1355+
end,
1356+
},
1357+
})
1358+
end,
1359+
},
1360+
},
1361+
strategies = {
1362+
chat = {
1363+
adapter = "llama.cpp",
1364+
},
1365+
inline = {
1366+
adapter = "llama.cpp",
1367+
},
1368+
},
1369+
})
1370+
<
1371+
1372+
12901373
CHAT BUFFER *codecompanion-configuration-chat-buffer*
12911374

12921375
By default, CodeCompanion provides a "chat" strategy that uses a dedicated
@@ -2869,7 +2952,7 @@ The fastest way to copy an LLM’s code output is with `gy`. This will yank the
28692952
nearest codeblock.
28702953

28712954

2872-
APPLYING AN LLM€�S EDITS TO A BUFFER OR FILE ~
2955+
APPLYING AN LLMS EDITS TO A BUFFER OR FILE ~
28732956

28742957
The |codecompanion-usage-chat-buffer-tools-files| tool, combined with the
28752958
|codecompanion-usage-chat-buffer-variables.html-buffer| variable or
@@ -4922,6 +5005,7 @@ These handlers parse LLM responses:
49225005
- `response.parse_chat` - Format chat output for the chat buffer
49235006
- `response.parse_inline` - Format output for inline insertion
49245007
- `response.parse_tokens` - Extract token count from the response
5008+
- `response.parse_meta` - Process non-standard fields in the response (currently only supported by OpenAI-based adapters)
49255009

49265010

49275011
TOOL HANDLERS
@@ -4936,7 +5020,7 @@ These handlers manage tool/function calling:
49365020
as a great reference to understand how they’re working with the output of the
49375021
API
49385022

4939-
OPENAI€�S API OUTPUT
5023+
OPENAIS API OUTPUT
49405024

49415025
If we reference the OpenAI documentation
49425026
<https://platform.openai.com/docs/guides/text-generation/chat-completions-api>
@@ -5141,6 +5225,53 @@ we have data in our response:
51415225
<
51425226

51435227

5228+
RESPONSE.PARSE_META
5229+
5230+
Some OpenAI-compatible API providers like deepseek, Gemini and OpenRouter
5231+
implement a superset of the standard specification, and provide reasoning
5232+
tokens/summaries within their response. The non-standard fields in the
5233+
`message` (non-streaming)
5234+
<https://platform.openai.com/docs/api-reference/chat/object#chat-object-choices-message>
5235+
or `delta` (streaming)
5236+
<https://platform.openai.com/docs/api-reference/chat-streaming/streaming#chat_streaming-streaming-choices-delta>
5237+
object are captured by the OpenAI adapter and can be used to extract the
5238+
reasoning.
5239+
5240+
For example, the DeepSeek API provides the reasoning tokens in the
5241+
`delta.reasoning_content` field. We can therefore use the following
5242+
`parse_meta` handler to extract the reasoning tokens and put them into the
5243+
appropriate output fields:
5244+
5245+
>lua
5246+
handlers = {
5247+
response = {
5248+
---@param self CodeCompanion.HTTPAdapter
5249+
--- `data` is the output of the `parse_chat` handler
5250+
---@param data {status: string, output: {role: string?, content: string?}, extra: table}
5251+
---@return {status: string, output: {role: string?, content: string?, reasoning:{content: string?}?}}
5252+
parse_meta = function(self, data)
5253+
local extra = data.extra
5254+
if extra.reasoning_content then
5255+
-- codecompanion expect the reasoning tokens in this format
5256+
data.output.reasoning = { content = extra.reasoning_content }
5257+
-- so that codecompanion doesn't mistake this as a normal response with empty string as the content
5258+
if data.output.content == "" then
5259+
data.output.content = nil
5260+
end
5261+
end
5262+
return data
5263+
end
5264+
}
5265+
}
5266+
<
5267+
5268+
Notes:
5269+
5270+
1. You don’t always have to set `data.output.content` to `nil`. This is mostly intended for `streaming`, and you may encounter issues in non-stream mode if you do that.
5271+
2. It’s expected that the processed `data` table is returned at the end.
5272+
3. For adapters that are using the legacy flat handler formats, this handler should be named `handlers.parse_message_meta`. The function signature stays the same.
5273+
5274+
51445275
REQUEST.BUILD_PARAMETERS
51455276

51465277
For the purposes of the OpenAI adapter, no additional parameters need to be
@@ -6796,7 +6927,7 @@ tool to function. In the case of Anthropic, we insert additional headers.
67966927
<
67976928

67986929
Some adapter tools can be a `hybrid` in terms of their implementation. That is,
6799-
they’re an adapter tool that requires a client-side component (i.e.a
6930+
they’re an adapter tool that requires a client-side component (i.e. a
68006931
built-in tool). This is the case for the
68016932
|codecompanion-usage-chat-buffer-tools-memory| tool from Anthropic. To allow
68026933
for this, ensure that the tool definition in `available_tools` has

doc/configuration/adapters.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,82 @@ require("codecompanion").setup({
378378
}),
379379
```
380380

381+
## Setup: OpenRouter with Reasoning Output
382+
383+
```lua
384+
require("codecompanion").setup({
385+
adapters = {
386+
http = {
387+
openrouter = function()
388+
return require("codecompanion.adapters").extend("openai_compatible", {
389+
env = {
390+
url = "https://openrouter.ai/api",
391+
api_key = "OPENROUTER_API_KEY",
392+
chat_url = "/v1/chat/completions",
393+
},
394+
handlers = {
395+
parse_message_meta = function(self, data)
396+
local extra = data.extra
397+
if extra and extra.reasoning then
398+
data.output.reasoning = { content = extra.reasoning }
399+
if data.output.content == "" then
400+
data.output.content = nil
401+
end
402+
end
403+
return data
404+
end,
405+
},
406+
})
407+
end,
408+
},
409+
},
410+
strategies = {
411+
chat = {
412+
adapter = "openrouter",
413+
},
414+
inline = {
415+
adapter = "openrouter",
416+
},
417+
},
418+
})
419+
```
420+
421+
## Setup: llama.cpp with `--reasoning-format deepseek`
422+
423+
```lua
424+
require("codecompanion").setup({
425+
adapters = {
426+
http = {
427+
["llama.cpp"] = function()
428+
return require("codecompanion.adapters").extend("openai_compatible", {
429+
env = {
430+
url = "http://127.0.0.1:8080", -- replace with your llama.cpp instance
431+
api_key = "TERM",
432+
chat_url = "/v1/chat/completions",
433+
},
434+
handlers = {
435+
parse_message_meta = function(self, data)
436+
local extra = data.extra
437+
if extra and extra.reasoning_content then
438+
data.output.reasoning = { content = extra.reasoning_content }
439+
if data.output.content == "" then
440+
data.output.content = nil
441+
end
442+
end
443+
return data
444+
end,
445+
},
446+
})
447+
end,
448+
},
449+
},
450+
strategies = {
451+
chat = {
452+
adapter = "llama.cpp",
453+
},
454+
inline = {
455+
adapter = "llama.cpp",
456+
},
457+
},
458+
})
459+
```

doc/extending/adapters.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ These handlers parse LLM responses:
176176
- `response.parse_chat` - Format chat output for the chat buffer
177177
- `response.parse_inline` - Format output for inline insertion
178178
- `response.parse_tokens` - Extract token count from the response
179+
- `response.parse_meta` - Process non-standard fields in the response (currently only supported by OpenAI-based adapters)
179180

180181
### Tool Handlers
181182

@@ -376,6 +377,43 @@ handlers = {
376377
}
377378
```
378379

380+
### `response.parse_meta`
381+
382+
Some OpenAI-compatible API providers like deepseek, Gemini and OpenRouter implement a superset of the standard specification, and provide reasoning tokens/summaries within their response.
383+
The non-standard fields in the [`message` (non-streaming)](https://platform.openai.com/docs/api-reference/chat/object#chat-object-choices-message) or [`delta` (streaming)](https://platform.openai.com/docs/api-reference/chat-streaming/streaming#chat_streaming-streaming-choices-delta) object are captured by the OpenAI adapter and can be used to extract the reasoning.
384+
385+
For example, the DeepSeek API provides the reasoning tokens in the `delta.reasoning_content` field.
386+
We can therefore use the following `parse_meta` handler to extract the reasoning tokens and put them into the appropriate output fields:
387+
388+
```lua
389+
handlers = {
390+
response = {
391+
---@param self CodeCompanion.HTTPAdapter
392+
--- `data` is the output of the `parse_chat` handler
393+
---@param data {status: string, output: {role: string?, content: string?}, extra: table}
394+
---@return {status: string, output: {role: string?, content: string?, reasoning:{content: string?}?}}
395+
parse_meta = function(self, data)
396+
local extra = data.extra
397+
if extra.reasoning_content then
398+
-- codecompanion expect the reasoning tokens in this format
399+
data.output.reasoning = { content = extra.reasoning_content }
400+
-- so that codecompanion doesn't mistake this as a normal response with empty string as the content
401+
if data.output.content == "" then
402+
data.output.content = nil
403+
end
404+
end
405+
return data
406+
end
407+
}
408+
}
409+
```
410+
411+
Notes:
412+
413+
1. You don't always have to set `data.output.content` to `nil`. This is mostly intended for `streaming`, and you may encounter issues in non-stream mode if you do that.
414+
2. It's expected that the processed `data` table is returned at the end.
415+
3. For adapters that are using the legacy flat handler formats, this handler should be named `handlers.parse_message_meta`. The function signature stays the same.
416+
379417
### `request.build_parameters`
380418

381419
For the purposes of the OpenAI adapter, no additional parameters need to be created. So we just pass this through:

0 commit comments

Comments
 (0)