Skip to content

Commit d1ee639

Browse files
authored
Merge pull request #234 from ArthurHeymans/Gemini-pro-3-preview
feat: support Google Gemini thought signatures + gemini 3 support
2 parents eb122c0 + ef49a05 commit d1ee639

File tree

11 files changed

+201
-129
lines changed

11 files changed

+201
-129
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Support Google Gemini thought signatures.
6+
- Support `gemini-3-pro-preview` model.
57
- Support `~` in dynamic string parser.
68
- Support removing nullable values from LLM request body if the value in extraPayload is null. #232
79

integration-test/integration/chat/github_copilot_test.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@
220220
:origin "native"
221221
:id "tool-1"
222222
:name "directory_tree"
223-
:argumentsText (str "h\":\"" (h/project-path->canon-path "resources") "\"}")
223+
:argumentsText (str "h\":\"" (h/json-escape-path (h/project-path->canon-path "resources")) "\"}")
224224
:manualApproval false
225225
:summary "Listing file tree"})
226226
(match-content chat-id "system" {:type "usage"

integration-test/integration/chat/google_test.clj

Lines changed: 94 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -173,111 +173,101 @@
173173
:instructions (m/pred string?)}
174174
(llm.mocks/get-req-body :reasoning-1)))))))
175175

176-
#_(deftest tool-calling
177-
(eca/start-process!)
176+
(deftest tool-calling-with-thought-signatures
177+
(eca/start-process!)
178178

179-
(eca/request! (fixture/initialize-request))
180-
(eca/notify! (fixture/initialized-notification))
181-
(let [chat-id* (atom nil)]
182-
(testing "We ask what files LLM see"
183-
(llm.mocks/set-case! :tool-calling-0)
184-
(let [0
185-
resp (eca/request! (fixture/chat-prompt-request
186-
{:model "google/gemini-2.5-pro"
187-
:message "What files you see?"}))
188-
chat-id (reset! chat-id* (:chatId resp))]
179+
(eca/request! (fixture/initialize-request))
180+
(eca/notify! (fixture/initialized-notification))
181+
(llm-mock.openai-chat/set-thinking-tag! "thought")
182+
(let [chat-id* (atom nil)]
183+
(testing "We ask what files LLM sees - tool call includes thought signature"
184+
(llm.mocks/set-case! :tool-calling-with-thought-signature-0)
185+
(let [resp (eca/request! (fixture/chat-prompt-request
186+
{:model "google/gemini-2.5-pro"
187+
:message "What files you see?"}))
188+
chat-id (reset! chat-id* (:chatId resp))]
189189

190-
(is (match?
191-
{:chatId (m/pred string?)
192-
:model "google/gemini-2.5-pro"
193-
:status "prompting"}
194-
resp))
190+
(is (match?
191+
{:chatId (m/pred string?)
192+
:model "google/gemini-2.5-pro"
193+
:status "prompting"}
194+
resp))
195+
196+
(match-content chat-id "user" {:type "text" :text "What files you see?\n"})
197+
(match-content chat-id "system" {:type "metadata" :title "Some Cool Title"})
198+
(match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"})
199+
(match-content chat-id "system" {:type "progress" :state "running" :text "Generating"})
200+
(match-content chat-id "assistant" {:type "reasonStarted" :id (m/pred string?)})
201+
;; Note: The buffering in process-text-think-aware keeps a 9-char tail to detect </thought>,
202+
;; so chunks get re-split during streaming. The mock sends "I s", "hould call tool", " eca__directory_tree"
203+
;; but after buffering we get these chunks:
204+
(match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "I should "})
205+
(match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "call tool eca__direc"})
206+
(match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "tory_tree"})
207+
(match-content chat-id "assistant" {:type "reasonFinished" :id (m/pred string?) :totalTimeMs (m/pred number?)})
208+
;; Text is buffered (8-char tail for <thought> detection), then flushed when tool calls start
209+
(match-content chat-id "assistant" {:type "text" :text "I will li"})
210+
(match-content chat-id "assistant" {:type "text" :text "st files"})
211+
(match-content chat-id "assistant" {:type "toolCallPrepare"
212+
:origin "native"
213+
:id (m/pred string?)
214+
:name "directory_tree"
215+
:argumentsText ""
216+
:summary "Listing file tree"})
217+
(match-content chat-id "assistant" {:type "toolCallPrepare"
218+
:origin "native"
219+
:id (m/pred string?)
220+
:name "directory_tree"
221+
:argumentsText "{\"pat"
222+
:summary "Listing file tree"})
223+
(match-content chat-id "assistant" {:type "toolCallPrepare"
224+
:origin "native"
225+
:id (m/pred string?)
226+
:name "directory_tree"
227+
:argumentsText (str "h\":\"" (h/json-escape-path (h/project-path->canon-path "resources")) "\"}")
228+
:summary "Listing file tree"})
229+
(match-content chat-id "system" {:type "usage"})
230+
(match-content chat-id "assistant" {:type "toolCallRun"
231+
:origin "native"
232+
:id (m/pred string?)
233+
:name "directory_tree"
234+
:arguments {:path (h/project-path->canon-path "resources")}
235+
:manualApproval false
236+
:summary "Listing file tree"})
237+
(match-content chat-id "assistant" {:type "toolCallRunning"
238+
:origin "native"
239+
:id (m/pred string?)
240+
:name "directory_tree"
241+
:arguments {:path (h/project-path->canon-path "resources")}
242+
:summary "Listing file tree"})
243+
(match-content chat-id "system" {:type "progress" :state "running" :text "Calling tool"})
244+
(match-content chat-id "assistant" {:type "toolCalled"
245+
:origin "native"
246+
:id (m/pred string?)
247+
:name "directory_tree"
248+
:arguments {:path (h/project-path->canon-path "resources")}
249+
:summary "Listing file tree"
250+
:totalTimeMs (m/pred number?)
251+
:error false
252+
:outputs [{:type "text" :text (str (h/project-path->canon-path "resources") "\n"
253+
" file1.md\n"
254+
" file2.md\n\n"
255+
"0 directories, 2 files")}]})
256+
;; Text chunks get re-split due to 8-char tail buffering for <thought> detection.
257+
;; Note: We use m/in-any-order for the final text/usage/progress events since their
258+
;; relative ordering can vary due to async processing and buffering.
259+
(match-content chat-id "assistant" {:type "text" :text "The files"})
260+
(match-content chat-id "assistant" {:type "text" :text " I see:\nfile"})
261+
(match-content chat-id "assistant" {:type "text" :text "1\nfile2\n"})
262+
(match-content chat-id "system" {:type "progress" :state "finished"})
195263

196-
(match-content chat-id "user" {:type "text" :text "What files you see?\n"})
197-
(match-content chat-id "system" {:type "progress" :state "running" :text "Waiting model"})
198-
(match-content chat-id "system" {:type "progress" :state "running" :text "Generating"})
199-
(match-content chat-id "assistant" {:type "reasonStarted" :id (m/pred string?)})
200-
(match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text "I should call tool"})
201-
(match-content chat-id "assistant" {:type "reasonText" :id (m/pred string?) :text " eca__directory_tree"})
202-
(match-content chat-id "assistant" {:type "reasonFinished" :id (m/pred string?) :totalTimeMs (m/pred number?)})
203-
(match-content chat-id "assistant" {:type "text" :text "I will list files"})
204-
(match-content chat-id "assistant" {:type "toolCallPrepare"
205-
:origin "native"
206-
:id "tool-1"
207-
:name "directory_tree"
208-
:argumentsText ""
209-
:manualApproval false
210-
:summary "Listing file tree"})
211-
(match-content chat-id "assistant" {:type "toolCallPrepare"
212-
:origin "native"
213-
:id "tool-1"
214-
:name "directory_tree"
215-
:argumentsText "{\"pat"
216-
:manualApproval false
217-
:summary "Listing file tree"})
218-
(match-content chat-id "assistant" {:type "toolCallPrepare"
219-
:origin "native"
220-
:id "tool-1"
221-
:name "directory_tree"
222-
:argumentsText (str "h\":\"" (h/project-path->canon-path "resources") "\"}")
223-
:manualApproval false
224-
:summary "Listing file tree"})
225-
(match-content chat-id "system" {:type "usage"
226-
:messageInputTokens 5
227-
:messageOutputTokens 30
228-
:sessionTokens 35
229-
:messageCost (m/pred string?)
230-
:sessionCost (m/pred string?)})
231-
(match-content chat-id "assistant" {:type "toolCallRun"
232-
:origin "native"
233-
:id "tool-1"
234-
:name "directory_tree"
235-
:arguments {:path (h/project-path->canon-path "resources")}
236-
:manualApproval false
237-
:summary "Listing file tree"})
238-
(match-content chat-id "assistant" {:type "toolCallRunning"
239-
:origin "native"
240-
:id "tool-1"
241-
:name "directory_tree"
242-
:arguments {:path (h/project-path->canon-path "resources")}
243-
:totalTimeMs number?
244-
:summary "Listing file tree"})
245-
(match-content chat-id "assistant" {:type "toolCalled"
246-
:origin "native"
247-
:id "tool-1"
248-
:name "directory_tree"
249-
:arguments {:path (h/project-path->canon-path "resources")}
250-
:summary "Listing file tree"
251-
:error false
252-
:outputs [{:type "text" :text (str "[FILE] " (h/project-path->canon-path "resources/file1.md\n")
253-
"[FILE] " (h/project-path->canon-path "resources/file2.md\n"))}]})
254-
(match-content chat-id "assistant" {:type "text" :text "The files I see:\n"})
255-
(match-content chat-id "assistant" {:type "text" :text "file1\nfile2\n"})
256-
(match-content chat-id "system" {:type "usage"
257-
:messageInputTokens 5
258-
:messageOutputTokens 30
259-
:sessionTokens 70
260-
:messageCost (m/pred string?)
261-
:sessionCost (m/pred string?)})
262-
(match-content chat-id "system" {:type "progress" :state "finished"})
264+
;; Verify thought signature was passed back in the second request
265+
(let [raw-messages (llm.mocks/get-raw-messages :tool-calling-with-thought-signature-0)
266+
;; Find the assistant message with tool_calls
267+
assistant-tool-call-msg (first (filter #(and (= "assistant" (:role %))
268+
(seq (:tool_calls %)))
269+
raw-messages))]
263270
(is (match?
264-
{:messages [{:role "user" :content [{:type "text" :text "What files you see?"}]}
265-
{:role "assistant"
266-
:content [{:type "thinking"
267-
:signature "enc-123"
268-
:thinking "I should call tool eca__directory_tree"}]}
269-
{:role "assistant" :content [{:type "text" :text "I will list files"}]}
270-
{:role "assistant"
271-
:content [{:type "tool_use"
272-
:id "tool-1"
273-
:name "eca__directory_tree"
274-
:input {:path (h/project-path->canon-path "resources")}}]}
275-
{:role "user"
276-
:content [{:type "tool_result"
277-
:tool_use_id "tool-1"
278-
:content (str "[FILE] " (h/project-path->canon-path "resources/file1.md\n")
279-
"[FILE] " (h/project-path->canon-path "resources/file2.md\n\n"))}]}]
280-
:tools (m/embeds
281-
[{:name "eca__directory_tree"}])
282-
:system (m/pred vector?)}
283-
llm.mocks/*last-req-body*))))))
271+
{:role "assistant"
272+
:tool_calls [{:extra_content {:google {:thought_signature "thought-sig-abc123"}}}]}
273+
assistant-tool-call-msg)))))))

integration-test/integration/chat/openai_test.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@
230230
:origin "native"
231231
:id "tool-1"
232232
:name "directory_tree"
233-
:argumentsText (str "h\":\"" (h/project-path->canon-path "resources") "\"}")
233+
:argumentsText (str "h\":\"" (h/json-escape-path (h/project-path->canon-path "resources")) "\"}")
234234
:summary "Listing file tree"})
235235
(match-content chat-id "system" {:type "usage"
236236
:sessionTokens 35

integration-test/integration/initialize_test.clj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"github-copilot/grok-code-fast-1"
3131
"google/gemini-2.0-flash"
3232
"google/gemini-2.5-pro"
33+
"google/gemini-3-pro-preview"
3334
"openai/gpt-4.1"
3435
"openai/gpt-5"
3536
"openai/gpt-5-codex"
@@ -94,6 +95,7 @@
9495
"github-copilot/grok-code-fast-1"
9596
"google/gemini-2.0-flash"
9697
"google/gemini-2.5-pro"
98+
"google/gemini-3-pro-preview"
9799
"my-custom/bar2"
98100
"my-custom/foo1"
99101
"openai/gpt-4.1"

integration-test/llm_mock/mocks.clj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,22 @@
66
(alter-var-root #'*case* (constantly case)))
77

88
(defonce ^:private req-bodies* (atom {}))
9+
(defonce ^:private raw-messages* (atom {}))
910

1011
(defn set-req-body! [mock-case-id body]
1112
(swap! req-bodies* assoc mock-case-id body))
1213

1314
(defn get-req-body [mock-case-id]
1415
(get @req-bodies* mock-case-id))
1516

17+
(defn set-raw-messages! [mock-case-id messages]
18+
(swap! raw-messages* assoc mock-case-id messages))
19+
20+
(defn get-raw-messages [mock-case-id]
21+
(get @raw-messages* mock-case-id))
22+
1623
(defn clean-req-bodies! []
17-
(reset! req-bodies* {}))
24+
(reset! req-bodies* {})
25+
(reset! raw-messages* {}))
1826

1927
(def chat-title-generator-str "Title generator")

integration-test/llm_mock/openai.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@
152152
(sse-send! ch "response.function_call_arguments.delta"
153153
{:type "response.function_call_arguments.delta"
154154
:item_id "item-1"
155-
:delta (str "h\":\"" (h/project-path->canon-path "resources") "\"}")})
155+
:delta (str "h\":\"" (h/json-escape-path (h/project-path->canon-path "resources")) "\"}")})
156156
;; Complete with the function call present so the client triggers tools
157157
(sse-send! ch "response.completed"
158158
{:type "response.completed"

0 commit comments

Comments
 (0)