Skip to content

Commit a8d3f08

Browse files
ericdalloeca-agent
andcommitted
Fix OpenAI models stuck at toolCallPrepare during tool execution
Some OpenAI models stream tool calls via events but return empty :output in response.completed. Fall back to accumulated tool call data from response.output_item.done when this happens. Closes #398 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca <git@eca.dev>
1 parent 25e085a commit a8d3f08

File tree

2 files changed

+24
-14
lines changed

2 files changed

+24
-14
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- Add `chatRetentionDays` config to control chat and cache cleanup retention period, default changed from 7 to 14 days. Set to 0 to disable cleanup. #393
66
- Preserve full chat history across compactions using tombstone markers instead of replacing messages. #394
77
- Add message flags — named checkpoints for resuming and forking chats. #395
8+
- Fix OpenAI models getting stuck at toolCallPrepare when streaming response returns empty output in response.completed. #398
89

910
## 0.123.3
1011

src/eca/llm_providers/openai.clj

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@
199199
(on-reason {:status :finished
200200
:id (-> data :item :id)
201201
:external-id (-> data :item :encrypted_content)}))
202+
"function_call" (swap! tool-call-by-item-id* update (-> data :item :id)
203+
assoc :arguments (-> data :item :arguments))
202204
"web_search_call" (on-server-web-search {:status :finished
203205
:id (-> data :item :id)
204206
:output nil})
@@ -246,20 +248,27 @@
246248
;; done
247249
"response.completed"
248250
(let [response (:response data)
249-
tool-calls (keep (fn [{:keys [id call_id name arguments] :as output}]
250-
(when (= "function_call" (:type output))
251-
;; Fallback case when the tool call was not prepared before when
252-
;; some models/apis respond only with response.completed (skipping streaming).
253-
(when-not (get @tool-call-by-item-id* id)
254-
(swap! tool-call-by-item-id* assoc id {:full-name name :id call_id})
255-
(on-prepare-tool-call {:id call_id
256-
:full-name name
257-
:arguments-text arguments}))
258-
{:id call_id
259-
:item-id id
260-
:full-name name
261-
:arguments (json/parse-string arguments)}))
262-
(:output response))]
251+
tool-calls (or (seq (keep (fn [{:keys [id call_id name arguments] :as output}]
252+
(when (= "function_call" (:type output))
253+
(when-not (get @tool-call-by-item-id* id)
254+
(swap! tool-call-by-item-id* assoc id {:full-name name :id call_id})
255+
(on-prepare-tool-call {:id call_id
256+
:full-name name
257+
:arguments-text arguments}))
258+
{:id call_id
259+
:item-id id
260+
:full-name name
261+
:arguments (json/parse-string arguments)}))
262+
(:output response)))
263+
;; Fallback: some models stream tool calls via events
264+
;; but return empty :output in response.completed
265+
(seq (keep (fn [[item-id {:keys [full-name id arguments]}]]
266+
(when arguments
267+
{:id id
268+
:item-id item-id
269+
:full-name full-name
270+
:arguments (json/parse-string arguments)}))
271+
@tool-call-by-item-id*)))]
263272
(on-usage-updated (let [input-cache-read-tokens (-> response :usage :input_tokens_details :cached_tokens)]
264273
{:input-tokens (if input-cache-read-tokens
265274
(- (-> response :usage :input_tokens) input-cache-read-tokens)

0 commit comments

Comments
 (0)