Skip to content

Commit 9258a77

Browse files
committed
Support custom think tag start and end for openai-chat models via think-tag-start and think-tag-end provider configs.
Fixes #188
1 parent 6969f56 commit 9258a77

File tree

6 files changed

+31
-21
lines changed

6 files changed

+31
-21
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 custom think tag start and end for openai-chat models via `think-tag-start` and `think-tag-end` provider configs. #188
6+
57
## 0.75.2
68

79
- Add missing models supported by Github Copilot

deps.edn

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
org.clojure/core.async {:mvn/version "1.8.741"}
44
org.babashka/cli {:mvn/version "0.8.65"}
55
com.github.clojure-lsp/jsonrpc4clj {:mvn/version "1.0.2"}
6-
io.modelcontextprotocol.sdk/mcp {:mvn/version "0.14.1"}
6+
io.modelcontextprotocol.sdk/mcp {:mvn/version "0.15.0"}
77
borkdude/dynaload {:mvn/version "0.3.5"}
88
babashka/fs {:mvn/version "0.5.26"}
99
babashka/process {:mvn/version "0.6.23"}

docs/configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,8 @@ To configure, add your OTLP collector config via `:otlp` map following [otlp aut
433433
keyEnv?: string;
434434
keyRc?: string; // credential file lookup in format [login@]machine[:port]
435435
completionUrlRelativePath?: string;
436+
thinkTagStart?: string;
437+
thinkTagEnd?: string;
436438
models: {[key: string]: {
437439
modelName?: string;
438440
extraPayload?: {[key: string]: any}

docs/models.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Schema:
7070
| `keyRc` | string | Lookup specification to read the API key from Unix RC [credential files](#credential-file-authentication) | No* |
7171
| `key` | string | Direct API key (use instead of `keyEnv`) | No* |
7272
| `completionUrlRelativePath` | string | Optional override for the completion endpoint path (see defaults below and examples like Azure) | No |
73+
| `thinkTagStart` | string | Optional override the think start tag tag for openai-chat (Default: "<think>") api | No |
74+
| `thinkTagEnd` | string | Optional override the think end tag for openai-chat (Default: "</think>") api | No |
7375
| `models` | map | Key: model name, value: its config | Yes |
7476
| `models <model> extraPayload` | map | Extra payload sent in body to LLM | No |
7577
| `models <model> modelName` | string | Override model name, useful to have multiple models with different configs and names that use same LLM model | No |

src/eca/llm_api.clj

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@
163163
:supports-image? supports-image?
164164
:past-messages past-messages
165165
:tools tools
166-
:thinking-tag "thought"
166+
:think-tag-start "<thought>"
167+
:think-tag-end "</thought>"
167168
:extra-payload (merge {:parallel_tool_calls false}
168169
(when reason?
169170
{:extra_body {:google {:thinking_config {:include_thoughts true}}}})
@@ -192,7 +193,9 @@
192193
"anthropic" llm-providers.anthropic/chat!
193194
"openai-chat" llm-providers.openai-chat/chat-completion!
194195
(on-error {:message (format "Unknown model %s for provider %s" (:api provider-config) provider)}))
195-
url-relative-path (:completionUrlRelativePath provider-config)]
196+
url-relative-path (:completionUrlRelativePath provider-config)
197+
think-tag-start (:thinkTagStart provider-config)
198+
think-tag-end (:thinkTagEnd provider-config)]
196199
(provider-fn
197200
{:model real-model
198201
:instructions instructions
@@ -205,6 +208,8 @@
205208
:tools tools
206209
:extra-payload extra-payload
207210
:url-relative-path url-relative-path
211+
:think-tag-start think-tag-start
212+
:think-tag-end think-tag-end
208213
:api-url api-url
209214
:api-key api-key}
210215
callbacks))

src/eca/llm_providers/openai_chat.clj

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126

127127
(defn ^:private transform-message
128128
"Transform a single ECA message to OpenAI format. Returns nil for unsupported roles."
129-
[{:keys [role content] :as _msg} supports-image? thinking-start-tag thinking-end-tag]
129+
[{:keys [role content] :as _msg} supports-image? think-tag-start think-tag-end]
130130
(case role
131131
"tool_call" {:type :tool-call ; Special marker for accumulation
132132
:data {:id (:id content)
@@ -140,7 +140,7 @@
140140
:content (extract-content content supports-image?)}
141141
"reason" {:role "assistant"
142142
:content [{:type "text"
143-
:text (str thinking-start-tag (:text content) thinking-end-tag)}]}
143+
:text (str think-tag-start (:text content) think-tag-end)}]}
144144
"assistant" {:role "assistant"
145145
:content (extract-content content supports-image?)}
146146
"system" {:role "system"
@@ -193,9 +193,9 @@
193193
'assistant' role message, not as separate messages. This function ensures compliance
194194
with that requirement by accumulating tool calls and flushing them into assistant
195195
messages when a non-tool_call message is encountered."
196-
[messages supports-image? thinking-start-tag thinking-end-tag]
196+
[messages supports-image? think-tag-start think-tag-end]
197197
(->> messages
198-
(map #(transform-message % supports-image? thinking-start-tag thinking-end-tag))
198+
(map #(transform-message % supports-image? think-tag-start think-tag-end))
199199
(remove nil?)
200200
accumulate-tool-calls
201201
(filter valid-message?)))
@@ -228,9 +228,9 @@
228228
- Inside thinking: emit reasoning up to </think> and keep a small tail to detect split tags
229229
- When a tag boundary is found, open/close the reasoning block accordingly"
230230
[text content-buffer* reasoning-type* current-reason-id*
231-
reasoning-started* thinking-start-tag thinking-end-tag on-message-received on-reason]
232-
(let [start-len (count thinking-start-tag)
233-
end-len (count thinking-end-tag)
231+
reasoning-started* think-tag-start think-tag-end on-message-received on-reason]
232+
(let [start-len (count think-tag-start)
233+
end-len (count think-tag-end)
234234
;; Keep a small tail to detect tags split across chunk boundaries.
235235
start-tail (max 0 (dec start-len))
236236
end-tail (max 0 (dec end-len))
@@ -258,7 +258,7 @@
258258
(let [^String buf @content-buffer*]
259259
(if (= @reasoning-type* :tag)
260260
;; Inside a thinking block; look for end tag
261-
(let [idx (.indexOf buf ^String thinking-end-tag)]
261+
(let [idx (.indexOf buf ^String think-tag-end)]
262262
(if (>= idx 0)
263263
(let [before (.substring buf 0 idx)
264264
after (.substring buf (+ idx end-len))]
@@ -271,7 +271,7 @@
271271
(emit-think! (.substring buf 0 emit-len))
272272
(reset! content-buffer* (.substring buf emit-len))))))
273273
;; Outside a thinking block; look for start tag
274-
(let [idx (.indexOf buf ^String thinking-start-tag)]
274+
(let [idx (.indexOf buf ^String think-tag-start)]
275275
(if (>= idx 0)
276276
(let [before (.substring buf 0 idx)
277277
after (.substring buf (+ idx start-len))]
@@ -292,16 +292,15 @@
292292
Compatible with OpenRouter and other OpenAI-compatible providers."
293293
[{:keys [model user-messages instructions temperature api-key api-url url-relative-path
294294
past-messages tools extra-payload extra-headers supports-image?
295-
thinking-tag]
296-
:or {thinking-tag "think"}}
295+
think-tag-start think-tag-end]
296+
:or {think-tag-start "<think>"
297+
think-tag-end "</think>"}}
297298
{:keys [on-message-received on-error on-prepare-tool-call on-tools-called on-reason on-usage-updated] :as callbacks}]
298-
(let [thinking-start-tag (str "<" thinking-tag ">")
299-
thinking-end-tag (str "</" thinking-tag ">")
300-
stream? (boolean callbacks)
299+
(let [stream? (boolean callbacks)
301300
messages (vec (concat
302301
(when instructions [{:role "system" :content instructions}])
303-
(normalize-messages past-messages supports-image? thinking-start-tag thinking-end-tag)
304-
(normalize-messages user-messages supports-image? thinking-start-tag thinking-end-tag)))
302+
(normalize-messages past-messages supports-image? think-tag-start think-tag-end)
303+
(normalize-messages user-messages supports-image? think-tag-start think-tag-end)))
305304

306305
body (deep-merge
307306
(assoc-some
@@ -331,7 +330,7 @@
331330
(when-let [{:keys [new-messages]} (on-tools-called tools-to-call)]
332331
(let [new-messages-list (vec (concat
333332
(when instructions [{:role "system" :content instructions}])
334-
(normalize-messages new-messages supports-image? thinking-start-tag thinking-end-tag)))
333+
(normalize-messages new-messages supports-image? think-tag-start think-tag-end)))
335334
new-rid (llm-util/gen-rid)]
336335
(reset! tool-calls* {})
337336
(base-chat-request!
@@ -366,7 +365,7 @@
366365
;; Process content if present (with thinking blocks support)
367366
(when-let [ct (:content delta)]
368367
(process-text-think-aware ct content-buffer* reasoning-type* current-reason-id* reasoning-started*
369-
thinking-start-tag thinking-end-tag on-message-received on-reason))
368+
think-tag-start think-tag-end on-message-received on-reason))
370369

371370
;; Process reasoning if present (o1 models and compatible providers)
372371
(when-let [reasoning-text (or (:reasoning delta)

0 commit comments

Comments
 (0)