Skip to content

Commit 57a1239

Browse files
authored
fix(adapters): conform to standard tool structure (#3077)
1 parent 914ae50 commit 57a1239

8 files changed

Lines changed: 52 additions & 9 deletions

File tree

.codecompanion/adapters/adapters.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,25 @@ This structure provides clear separation of concerns:
108108
- **response**: Pure transformations for parsing responses (chat, inline, tokens)
109109
- **tools**: Tool-specific operations (formatting calls and responses)
110110

111+
### Canonical Tool-Result Shape
112+
113+
Tool result messages produced by `format_response` are stored in `chat.messages` and re-read by every adapter's `build_messages`/`form_messages`. To keep messages portable across adapters, every adapter must write to the same canonical shape:
114+
115+
```lua
116+
{
117+
role = "tool",
118+
content = output,
119+
tools = {
120+
call_id = tool_call.id, -- required: matches the LLM's tool call
121+
name = tool_call["function"].name, -- required: function name (Gemini uses this)
122+
is_error = false, -- optional: Anthropic uses this
123+
},
124+
opts = { visible = false },
125+
}
126+
```
127+
128+
Adapter-specific extras (e.g. OpenAI Responses' `id`) are allowed but ignored by other adapters. When reading these messages back, an adapter should treat any message with `role == "tool"` as a tool result — never gate on adapter-specific fields.
129+
111130
### Calling Handlers
112131

113132
Throughout CodeCompanion, handlers are called using the `adapters.call_handler()` function, which provides backwards compatibility:

lua/codecompanion/adapters/http/anthropic.lua

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,14 +245,13 @@ return {
245245
-- 6. Treat 'tool' role as user and convert tool results to Anthropic format
246246
if m.role == "tool" then
247247
m.role = self.roles.user
248-
-- Convert tool result from CodeCompanion format to Anthropic format
249-
if m.tools and m.tools.type == "tool_result" then
248+
if m.tools then
250249
-- Handle content that might already be in Anthropic's format
251250
if type(m.content) == "table" and m.content.type == "tool_result" then
252251
-- Already in Anthropic format, keep it as-is but ensure it's in an array
253252
m.content = { m.content }
254253
else
255-
-- Convert from CodeCompanion format to Anthropic format
254+
-- Convert from the canonical tool-result shape to Anthropic's format
256255
m.content = {
257256
{
258257
type = "tool_result",
@@ -624,9 +623,9 @@ return {
624623
role = "tool",
625624
content = output,
626625
tools = {
627-
type = "tool_result",
628626
call_id = tool_call.id,
629627
is_error = false,
628+
name = tool_call["function"].name,
630629
},
631630
-- Chat Buffer option: To tell the chat buffer that this shouldn't be visible
632631
opts = { visible = false },
@@ -667,7 +666,7 @@ return {
667666
["claude-haiku-4-5"] = {
668667
formatted_name = "Claude Haiku 4.5",
669668
meta = { context_window = 200000, max_tokens = 64000 },
670-
opts = { can_reason = true, has_vision = true },
669+
opts = { can_reason = false, has_vision = true },
671670
},
672671

673672
-- Legacy models

lua/codecompanion/adapters/http/openai.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,7 @@ return {
363363
role = self.roles.tool or "tool",
364364
tools = {
365365
call_id = tool_call.id,
366+
name = tool_call["function"].name,
366367
},
367368
content = output,
368369
opts = { visible = false },

lua/codecompanion/adapters/http/openai_responses.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,9 @@ return {
573573
return {
574574
role = self.roles.tool or "tool",
575575
tools = {
576-
id = tool_call.id,
577576
call_id = tool_call.call_id,
577+
id = tool_call.id,
578+
name = tool_call["function"].name,
578579
},
579580
content = output,
580581
opts = { visible = false },

lua/codecompanion/interactions/chat/init.lua

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -701,12 +701,33 @@ end
701701
---Change the adapter in the chat buffer
702702
---@param adapter string
703703
---@param cb? function
704+
---@return boolean swapped Whether the adapter was actually swapped
704705
function Chat:change_adapter(adapter, cb)
705706
local function fire()
706707
return utils.fire("ChatAdapter", { bufnr = self.bufnr, adapter = adapters.make_safe(self.adapter) })
707708
end
708709

709-
self.adapter = require("codecompanion.adapters").resolve(adapter)
710+
local new_adapter = require("codecompanion.adapters").resolve(adapter)
711+
712+
-- Block adapter swaps once tool calls or reasoning have happened. Adapter-
713+
-- specific state (tool-call signatures, encrypted reasoning blobs) cannot
714+
-- be carried into a different vendor's API. Model swaps within the same
715+
-- adapter are unaffected.
716+
if self.adapter.name ~= new_adapter.name then
717+
local has_state = vim.iter(self.messages or {}):any(function(m)
718+
return m.reasoning ~= nil or (m.tools and m.tools.calls ~= nil)
719+
end)
720+
if has_state then
721+
utils.notify(
722+
fmt("Adapter cannot be changed after tool executions. Start a new chat to use `%s`", new_adapter.name),
723+
vim.log.levels.WARN
724+
)
725+
return false
726+
end
727+
end
728+
729+
self.acp_connection = nil
730+
self.adapter = new_adapter
710731
self.ui.adapter = self.adapter
711732

712733
if self.adapter.type == "acp" then
@@ -722,6 +743,7 @@ function Chat:change_adapter(adapter, cb)
722743
self:update_metadata()
723744
self:apply_settings()
724745
fire()
746+
return true
725747
end
726748

727749
---Set a model in the chat buffer

lua/codecompanion/interactions/chat/keymaps/change_adapter.lua

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,6 @@ function M.callback(chat)
244244
end
245245

246246
if current_adapter ~= selected_adapter then
247-
chat.acp_connection = nil -- Ensure we reset this
248247
chat:change_adapter(selected_adapter, on_adapter_ready)
249248
else
250249
return on_adapter_ready()

tests/adapters/http/test_openai.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ T["OpenAI adapter"]["can output tool call"] = function()
156156
role = "tool",
157157
tools = {
158158
call_id = "call_RJU6xfk0OzQF3Gg9cOFS5RY7",
159+
name = "weather",
159160
},
160161
}, adapter.handlers.tools.output_response(adapter, tool_call, output))
161162
end

tests/adapters/http/test_openai_responses.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ T["Responses"]["can output tool calls"] = function()
5757
},
5858
role = "tool",
5959
tools = {
60-
id = "fc_0cf9af0f913994140068e27139a1948193bbf214a9664ec92c",
6160
call_id = "call_a9oyUMlFhnX8HvqzlfIx5Uek",
61+
id = "fc_0cf9af0f913994140068e27139a1948193bbf214a9664ec92c",
62+
name = "weather",
6263
},
6364
}, adapter.handlers.tools.format_response(adapter, tool_call, output))
6465
end

0 commit comments

Comments
 (0)