|
396 | 396 |
|
397 | 397 | ;; Atom to accumulate tool call data from streaming chunks. |
398 | 398 | ;; OpenAI streams tool call arguments across multiple chunks, so we need to |
399 | | - ;; accumulate the partial JSON strings before parsing them. Keys are either |
400 | | - ;; index numbers for simple cases, or "index-id" composite keys for parallel |
401 | | - ;; tool calls that share the same index but have different IDs. |
| 399 | + ;; accumulate partial JSON strings before parsing them. Keys are tool call |
| 400 | + ;; indices (fallback: IDs) to keep chunks grouped for the active response. |
402 | 401 | tool-calls* (atom {}) |
403 | 402 |
|
404 | 403 | ;; Reasoning state machine: |
|
431 | 430 | :content "" |
432 | 431 | :buffer "") |
433 | 432 | (on-reason {:status :started :id new-reason-id}))) |
| 433 | + find-existing-tool-key (fn [tool-calls index id] |
| 434 | + (some (fn [[k v]] (when (or (some-> id (= (:id v))) |
| 435 | + (and (nil? (:id v)) |
| 436 | + (some-> index (= (:index v))))) |
| 437 | + k)) |
| 438 | + tool-calls)) |
434 | 439 | on-tools-called-wrapper (fn on-tools-called-wrapper [tools-to-call on-tools-called handle-response] |
435 | 440 | (when-let [{:keys [new-messages]} (on-tools-called tools-to-call)] |
436 | 441 | (let [pruned-messages (prune-history new-messages) |
|
449 | 454 | :api-key api-key |
450 | 455 | :url-relative-path url-relative-path |
451 | 456 | :on-error wrapped-on-error |
452 | | - :on-stream (when stream? (fn [event data] (handle-response event data tool-calls* new-rid)))})))) |
| 457 | + :on-stream (when stream? (fn [event data] (handle-response event data tool-calls*)))})))) |
453 | 458 |
|
454 | | - handle-response (fn handle-response [event data tool-calls* rid] |
| 459 | + handle-response (fn handle-response [event data tool-calls*] |
455 | 460 | (if (= event "stream-end") |
456 | 461 | (do |
457 | 462 | ;; Flush any leftover buffered content and finish reasoning if needed |
|
496 | 501 | {name :name args :arguments} function |
497 | 502 | ;; Extract Google Gemini thought signature if present |
498 | 503 | thought-signature (get-in extra_content [:google :thought_signature]) |
499 | | - ;; Use RID as key to avoid collisions between API requests |
500 | | - tool-key (str rid "-" index) |
501 | | - ;; Create globally unique tool call ID for client |
502 | | - unique-id (when id (str rid "-" id))] |
503 | | - (when (and name unique-id) |
504 | | - (on-prepare-tool-call {:id unique-id |
505 | | - :full-name name |
506 | | - :arguments-text ""})) |
507 | | - (swap! tool-calls* update tool-key |
508 | | - (fn [existing] |
509 | | - (cond-> (or existing {:index index}) |
510 | | - unique-id (assoc :id unique-id) |
511 | | - name (assoc :full-name name) |
512 | | - args (update :arguments-text (fnil str "") args) |
513 | | - ;; Store thought signature for Google Gemini |
514 | | - thought-signature (assoc :external-id thought-signature)))) |
515 | | - (when-let [updated-tool-call (get @tool-calls* tool-key)] |
516 | | - (when (and (:id updated-tool-call) |
517 | | - (:full-name updated-tool-call) |
518 | | - args) |
519 | | - (on-prepare-tool-call (assoc updated-tool-call :arguments-text args))))))) |
| 504 | + existing-key (find-existing-tool-key @tool-calls* index id) |
| 505 | + existing (when existing-key (get @tool-calls* existing-key)) |
| 506 | + tool-key (or existing-key index id)] |
| 507 | + (if (nil? tool-key) |
| 508 | + (logger/warn logger-tag "Received tool_call delta without index/id; ignoring" |
| 509 | + {:tool-call tool-call}) |
| 510 | + (do |
| 511 | + (swap! tool-calls* update tool-key |
| 512 | + (fn [existing] |
| 513 | + (cond-> (or existing {:index index}) |
| 514 | + (some? index) (assoc :index index) |
| 515 | + (and id (nil? (:id existing))) (assoc :id id) |
| 516 | + (and name (nil? (:full-name existing))) (assoc :full-name name) |
| 517 | + args (update :arguments-text (fnil str "") args) |
| 518 | + ;; Store thought signature for Google Gemini |
| 519 | + thought-signature (assoc :external-id thought-signature)))) |
| 520 | + (when-let [updated-tool-call (get @tool-calls* tool-key)] |
| 521 | + ;; Streaming tool_calls may split metadata (id/name) and arguments across deltas. |
| 522 | + ;; Emit prepare once we can correlate the call (id + full-name), on first id or args deltas, |
| 523 | + ;; so :tool-prepare always precedes :tool-run in the tool-call state machine. |
| 524 | + (when (and (:id updated-tool-call) |
| 525 | + (:full-name updated-tool-call) |
| 526 | + (or (nil? (:id existing)) args)) |
| 527 | + (on-prepare-tool-call |
| 528 | + (assoc updated-tool-call |
| 529 | + :arguments-text (or args "")))))))))) |
520 | 530 | ;; Process finish reason if present (but not tool_calls which is handled above) |
521 | 531 | (when finish-reason |
522 | 532 | ;; Flush any leftover buffered content before finishing |
|
541 | 551 | :on-tools-called-wrapper on-tools-called-wrapper |
542 | 552 | :on-error wrapped-on-error |
543 | 553 | :on-stream (when stream? |
544 | | - (fn [event data] (handle-response event data tool-calls* rid)))}))) |
| 554 | + (fn [event data] (handle-response event data tool-calls*)))}))) |
0 commit comments