Skip to content

Commit d8f54ef

Browse files
committed
Fix deepseek reasoner flow
- some messages weren't constructed correctly and API invalidated them and stopped the flow.
1 parent 95e1540 commit d8f54ef

File tree

2 files changed

+38
-24
lines changed

2 files changed

+38
-24
lines changed

src/eca/llm_providers/openai_chat.clj

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
"reason" (if (:delta-reasoning? content)
159159
;; DeepSeek-style: reasoning_content must be passed back to API
160160
{:role "assistant"
161+
:content ""
161162
:reasoning_content (:text content)}
162163
;; Fallback: wrap in thinking tags for models that use text-based reasoning
163164
{:role "assistant"
@@ -169,30 +170,41 @@
169170
:content (extract-content content supports-image?)}
170171
nil))
171172

172-
(defn ^:private merge-reasoning-with-response
173-
"Merge reasoning_content from prev with tool_calls/content from msg.
174-
Required for DeepSeek which needs reasoning_content on same message as the response."
173+
(defn ^:private merge-assistant-messages
174+
"Merge two assistant messages into one.
175+
Concatenates contents and tool_calls, and preserves reasoning_content."
175176
[prev msg]
176-
(cond-> {:role "assistant"
177-
:reasoning_content (:reasoning_content prev)}
178-
;; content: from msg (DeepSeek's final answer)
179-
(:content msg)
180-
(assoc :content (:content msg))
181-
;; tool_calls: concatenate (prev may have some from earlier merge, msg adds more)
182-
(or (:tool_calls prev) (:tool_calls msg))
183-
(assoc :tool_calls (into (or (:tool_calls prev) []) (:tool_calls msg)))))
177+
(let [prev-content (:content prev)
178+
msg-content (:content msg)
179+
combined-content (cond
180+
(and (string? prev-content) (string? msg-content))
181+
(str prev-content msg-content)
182+
183+
(and (sequential? prev-content) (sequential? msg-content))
184+
(vec (concat prev-content msg-content))
185+
186+
:else
187+
(or msg-content prev-content ""))]
188+
(cond-> {:role "assistant"
189+
:content combined-content}
190+
(or (:reasoning_content prev) (:reasoning_content msg))
191+
(assoc :reasoning_content (str (:reasoning_content prev) (:reasoning_content msg)))
192+
193+
(or (:tool_calls prev) (:tool_calls msg))
194+
(assoc :tool_calls (vec (concat (:tool_calls prev)
195+
(:tool_calls msg)))))))
184196

185197
(defn ^:private merge-adjacent-assistants
186-
"Merge adjacent assistant messages only when prev has reasoning_content.
187-
This is required for DeepSeek which needs reasoning_content on same message as tool_calls/content."
198+
"Merge all adjacent assistant messages.
199+
This is required by many OpenAI-compatible APIs (including DeepSeek)
200+
which do not allow multiple consecutive assistant messages."
188201
[messages]
189202
(reduce
190203
(fn [acc msg]
191204
(let [prev (peek acc)]
192205
(if (and (= "assistant" (:role prev))
193-
(= "assistant" (:role msg))
194-
(:reasoning_content prev))
195-
(conj (pop acc) (merge-reasoning-with-response prev msg))
206+
(= "assistant" (:role msg)))
207+
(conj (pop acc) (merge-assistant-messages prev msg))
196208
(conj acc msg))))
197209
[]
198210
messages))

test/eca/llm_providers/openai_chat_test.clj

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
(def thinking-end-tag "</think>")
99

1010
(deftest normalize-messages-test
11-
(testing "With tool_call history - tool calls stay separate from preceding assistant (no reasoning_content)"
11+
(testing "With tool_call history - assistant text and tool calls are merged"
1212
(is (match?
1313
[{:role "user" :content [{:type "text" :text "List the files"}]}
14-
{:role "assistant" :content [{:type "text" :text "I'll list the files for you"}]}
1514
{:role "assistant"
15+
:content [{:type "text" :text "I'll list the files for you"}]
1616
:tool_calls [{:id "call-1"
1717
:type "function"
1818
:function {:name "eca__list_files"
@@ -36,11 +36,11 @@
3636
thinking-start-tag
3737
thinking-end-tag))))
3838

39-
(testing "Reason messages without reasoning-content use think tags, stay separate from following assistant"
39+
(testing "Reason messages without reasoning-content use think tags, merged with following assistant"
4040
(is (match?
4141
[{:role "user" :content [{:type "text" :text "Hello"}]}
42-
{:role "assistant" :content [{:type "text" :text "<think>Thinking...</think>"}]}
43-
{:role "assistant" :content [{:type "text" :text "Hi"}]}]
42+
{:role "assistant" :content [{:type "text" :text "<think>Thinking...</think>"}
43+
{:type "text" :text "Hi"}]}]
4444
(#'llm-providers.openai-chat/normalize-messages
4545
[{:role "user" :content "Hello"}
4646
{:role "reason" :content {:text "Thinking..."}}
@@ -149,11 +149,13 @@
149149
thinking-end-tag)))))
150150

151151
(deftest merge-adjacent-assistants-test
152-
(testing "Without reasoning_content, adjacent assistants are NOT merged"
152+
(testing "All adjacent assistant messages are merged (even without reasoning_content)"
153153
(is (match?
154154
[{:role "user" :content "What's the weather?"}
155-
{:role "assistant" :tool_calls [{:id "call-1" :function {:name "get_weather"}}]}
156-
{:role "assistant" :tool_calls [{:id "call-2" :function {:name "get_location"}}]}
155+
{:role "assistant"
156+
:content ""
157+
:tool_calls [{:id "call-1" :function {:name "get_weather"}}
158+
{:id "call-2" :function {:name "get_location"}}]}
157159
{:role "user" :content "Thanks"}]
158160
(#'llm-providers.openai-chat/merge-adjacent-assistants
159161
[{:role "user" :content "What's the weather?"}

0 commit comments

Comments
 (0)