Skip to content

Commit b7dc2b1

Browse files
authored
feat(adapters): add support for gemini-3 in the gemini adapter (#2411)
feat(adapters): add support for gemini-3 Co-authored-by: Oli Morris <[email protected]>
1 parent 9b38615 commit b7dc2b1

File tree

4 files changed

+149
-23
lines changed

4 files changed

+149
-23
lines changed

lua/codecompanion/adapters/http/gemini.lua

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,42 @@ return {
5757
return openai.handlers.form_tools(self, tools)
5858
end,
5959
form_messages = function(self, messages)
60-
return openai.handlers.form_messages(self, messages)
60+
local result = openai.handlers.form_messages(self, messages)
61+
62+
local STANDARD_TOOL_CALL_FIELDS = {
63+
"id",
64+
"type",
65+
"function",
66+
"_index",
67+
}
68+
69+
-- Post-process to preserve extra fields (like thought signatures)
70+
-- Ref: https://ai.google.dev/gemini-api/docs/thought-signatures#openai
71+
for _, msg in ipairs(result.messages) do
72+
local original_msg = nil
73+
for _, orig in ipairs(messages) do
74+
if orig.role == msg.role and orig.tools and orig.tools.calls then
75+
original_msg = orig
76+
break
77+
end
78+
end
79+
80+
-- If we have tool_calls in the original message then preserve non-standard fields
81+
if msg.tool_calls and original_msg and original_msg.tools and original_msg.tools.calls then
82+
for i, tool_call in ipairs(msg.tool_calls) do
83+
local original_tool = original_msg.tools.calls[i]
84+
if original_tool then
85+
for key, value in pairs(original_tool) do
86+
if not vim.tbl_contains(STANDARD_TOOL_CALL_FIELDS, key) then
87+
tool_call[key] = value
88+
end
89+
end
90+
end
91+
end
92+
end
93+
end
94+
95+
return result
6196
end,
6297
chat_output = function(self, data, tools)
6398
return openai.handlers.chat_output(self, data, tools)
@@ -86,6 +121,7 @@ return {
86121
desc = "The model that will complete your prompt. See https://ai.google.dev/gemini-api/docs/models/gemini#model-variations for additional details and options.",
87122
default = "gemini-2.5-flash",
88123
choices = {
124+
["gemini-3-pro-preview"] = { formatted_name = "Gemini 3 Pro", opts = { can_reason = true, has_vision = true } },
89125
["gemini-2.5-pro"] = { formatted_name = "Gemini 2.5 Pro", opts = { can_reason = true, has_vision = true } },
90126
["gemini-2.5-flash"] = { formatted_name = "Gemini 2.5 Flash", opts = { can_reason = true, has_vision = true } },
91127
["gemini-2.5-flash-preview-05-20"] = {

lua/codecompanion/adapters/http/openai.lua

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local adapter_utils = require("codecompanion.utils.adapters")
22
local log = require("codecompanion.utils.log")
3+
34
local CONSTANTS = {
45
STANDARD_MESSAGE_FIELDS = {
56
-- fields that are defined in the standard openai chat-completion API (inc. streaming and non-streaming)
@@ -14,9 +15,8 @@ local CONSTANTS = {
1415
}
1516

1617
---Find the non-standard fields in the `message` or `delta` that are not in the standard OpenAI chat-completion specs.
17-
---Returns `nil` if not found.
1818
---@param delta table?
19-
---@return table?
19+
---@return table|nil
2020
local function find_extra_fields(delta)
2121
if delta == nil then
2222
return nil
@@ -125,6 +125,7 @@ return {
125125
id = tool_call.id,
126126
["function"] = tool_call["function"],
127127
type = tool_call.type,
128+
-- Include a _meta field to hold everything else
128129
}
129130
end)
130131
:totable()
@@ -218,6 +219,40 @@ return {
218219
return nil
219220
end
220221

222+
-- Define standard tool_call fields
223+
local STANDARD_TOOL_CALL_FIELDS = {
224+
"id",
225+
"type",
226+
"function",
227+
"index",
228+
}
229+
230+
---Helper to create any tool data
231+
---@param tool table
232+
---@param index number
233+
---@param id string
234+
---@return table
235+
local function create_tool_data(tool, index, id)
236+
local tool_data = {
237+
_index = index,
238+
id = id,
239+
type = tool.type,
240+
["function"] = {
241+
name = tool["function"]["name"],
242+
arguments = tool["function"]["arguments"] or "",
243+
},
244+
}
245+
246+
-- Preserve any non-standard fields as-is
247+
for key, value in pairs(tool) do
248+
if not vim.tbl_contains(STANDARD_TOOL_CALL_FIELDS, key) then
249+
tool_data[key] = value
250+
end
251+
end
252+
253+
return tool_data
254+
end
255+
221256
-- Process tool calls from all choices
222257
if self.opts.tools and tools then
223258
for _, choice in ipairs(json.choices) do
@@ -248,26 +283,10 @@ return {
248283
end
249284

250285
if not found then
251-
table.insert(tools, {
252-
_index = tool_index,
253-
id = id,
254-
type = tool.type,
255-
["function"] = {
256-
name = tool["function"]["name"],
257-
arguments = tool["function"]["arguments"] or "",
258-
},
259-
})
286+
table.insert(tools, create_tool_data(tool, tool_index, id))
260287
end
261288
else
262-
table.insert(tools, {
263-
_index = i,
264-
id = id,
265-
type = tool.type,
266-
["function"] = {
267-
name = tool["function"]["name"],
268-
arguments = tool["function"]["arguments"],
269-
},
270-
})
289+
table.insert(tools, create_tool_data(tool, i, id))
271290
end
272291
end
273292
end
@@ -283,12 +302,12 @@ return {
283302
end
284303

285304
return {
286-
status = "success",
305+
extra = find_extra_fields(delta),
287306
output = {
288307
role = delta.role,
289308
content = delta.content,
290309
},
291-
extra = find_extra_fields(delta),
310+
status = "success",
292311
}
293312
end,
294313

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
data: {"choices":[{"delta":{"role":"assistant","tool_calls":[{"extra_content":{"google":{"thought_signature":"Eo123"}},"function":{"arguments":"{\"query\":\"README.md\"}","name":"file_search"},"id":"function-call-13802169321809035407","type":"function"}]},"index":0}],"created":1763566011,"id":"u-EdaYzRJ6zk7M8Pqdel4AU","model":"gemini-3-pro-preview","object":"chat.completion.chunk","usage":{"completion_tokens":18,"prompt_tokens":5171,"total_tokens":5239}}

tests/adapters/http/test_gemini.lua

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,76 @@ T["Gemini adapter"]["Streaming"]["can process tools"] = function()
106106
h.eq(tool_output, tools)
107107
end
108108

109+
T["Gemini adapter"]["Streaming"]["can preserve thought signatures in tool calls"] = function()
110+
local tools = {}
111+
local lines = vim.fn.readfile("tests/adapters/http/stubs/gemini_tools_thought_streaming.txt")
112+
for _, line in ipairs(lines) do
113+
adapter.handlers.chat_output(adapter, line, tools)
114+
end
115+
116+
local tool_output = {
117+
{
118+
_index = 1,
119+
["function"] = {
120+
arguments = '{"query":"README.md"}',
121+
name = "file_search",
122+
},
123+
id = "function-call-13802169321809035407",
124+
type = "function",
125+
extra_content = {
126+
google = {
127+
thought_signature = "Eo123",
128+
},
129+
},
130+
},
131+
}
132+
133+
h.eq(tool_output, tools)
134+
end
135+
136+
T["Gemini adapter"]["Streaming"]["can send thought signatures back in messages"] = function()
137+
local messages = {
138+
{
139+
content = "Search for README.md",
140+
role = "user",
141+
},
142+
{
143+
role = "assistant",
144+
tools = {
145+
calls = {
146+
{
147+
_index = 1,
148+
id = "function-call-13802169321809035407",
149+
type = "function",
150+
["function"] = {
151+
name = "file_search",
152+
arguments = '{"query":"README.md"}',
153+
},
154+
extra_content = {
155+
google = {
156+
thought_signature = "Eo123",
157+
},
158+
},
159+
},
160+
},
161+
},
162+
},
163+
{
164+
role = "user",
165+
tools = {
166+
call_id = "function-call-13802169321809035407",
167+
},
168+
content = '{"file":"README.md","contents":"..."}',
169+
},
170+
}
171+
172+
local output = adapter.handlers.form_messages(adapter, messages)
173+
174+
-- Verify the thought signature is preserved in the tool_calls
175+
local assistant_message = output.messages[2]
176+
h.eq("Eo123", assistant_message.tool_calls[1].extra_content.google.thought_signature)
177+
end
178+
109179
T["Gemini adapter"]["No Streaming"] = new_set({
110180
hooks = {
111181
pre_case = function()

0 commit comments

Comments
 (0)