|
158 | 158 | "reason" (if (:delta-reasoning? content) |
159 | 159 | ;; DeepSeek-style: reasoning_content must be passed back to API |
160 | 160 | {:role "assistant" |
| 161 | + :content "" |
161 | 162 | :reasoning_content (:text content)} |
162 | 163 | ;; Fallback: wrap in thinking tags for models that use text-based reasoning |
163 | 164 | {:role "assistant" |
|
169 | 170 | :content (extract-content content supports-image?)} |
170 | 171 | nil)) |
171 | 172 |
|
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." |
175 | 176 | [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 | + blank-string? (fn [s] (and (string? s) (string/blank? s))) |
| 180 | + combined-content (cond |
| 181 | + (nil? prev-content) |
| 182 | + msg-content |
| 183 | + |
| 184 | + (nil? msg-content) |
| 185 | + prev-content |
| 186 | + |
| 187 | + (blank-string? prev-content) |
| 188 | + msg-content |
| 189 | + |
| 190 | + (blank-string? msg-content) |
| 191 | + prev-content |
| 192 | + |
| 193 | + (and (string? prev-content) (string? msg-content)) |
| 194 | + (if (or (string/ends-with? prev-content "\n") |
| 195 | + (string/starts-with? msg-content "\n")) |
| 196 | + (str prev-content msg-content) |
| 197 | + (str prev-content "\n" msg-content)) |
| 198 | + |
| 199 | + (and (sequential? prev-content) (sequential? msg-content)) |
| 200 | + (vec (concat prev-content msg-content)) |
| 201 | + |
| 202 | + :else |
| 203 | + (let [as-seq (fn [c] (if (sequential? c) c [{:type "text" :text (str c)}]))] |
| 204 | + (vec (concat (as-seq prev-content) (as-seq msg-content)))))] |
| 205 | + (cond-> {:role "assistant" |
| 206 | + :content (or combined-content "")} |
| 207 | + (or (:reasoning_content prev) (:reasoning_content msg)) |
| 208 | + (assoc :reasoning_content (str (:reasoning_content prev) (:reasoning_content msg))) |
| 209 | + |
| 210 | + (or (:tool_calls prev) (:tool_calls msg)) |
| 211 | + (assoc :tool_calls (vec (concat (:tool_calls prev) |
| 212 | + (:tool_calls msg))))))) |
184 | 213 |
|
185 | 214 | (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." |
| 215 | + "Merge all adjacent assistant messages. |
| 216 | + This is required by many OpenAI-compatible APIs (including DeepSeek) |
| 217 | + which do not allow multiple consecutive assistant messages." |
188 | 218 | [messages] |
189 | 219 | (reduce |
190 | 220 | (fn [acc msg] |
191 | 221 | (let [prev (peek acc)] |
192 | 222 | (if (and (= "assistant" (:role prev)) |
193 | | - (= "assistant" (:role msg)) |
194 | | - (:reasoning_content prev)) |
195 | | - (conj (pop acc) (merge-reasoning-with-response prev msg)) |
| 223 | + (= "assistant" (:role msg))) |
| 224 | + (conj (pop acc) (merge-assistant-messages prev msg)) |
196 | 225 | (conj acc msg)))) |
197 | 226 | [] |
198 | 227 | messages)) |
199 | 228 |
|
200 | 229 | (defn ^:private valid-message? |
201 | 230 | "Check if a message should be included in the final output." |
202 | | - [{:keys [role content tool_calls reasoning_content] :as msg}] |
203 | | - (and msg |
204 | | - (or (= role "tool") ; Never remove tool messages |
| 231 | + [{:keys [role content tool_calls reasoning_content] :as _msg}] |
| 232 | + (and (or (= role "tool") ; Never remove tool messages |
205 | 233 | (seq tool_calls) ; Keep messages with tool calls |
206 | 234 | (seq reasoning_content) ; Keep messages with reasoning_content (DeepSeek) |
207 | 235 | (and (string? content) |
|
0 commit comments