|
15 | 15 |
|
16 | 16 | (def ^:private chat-completions-path "/chat/completions") |
17 | 17 |
|
| 18 | +(defn ^:private new-internal-tool-call-id [] |
| 19 | + (str (random-uuid))) |
| 20 | + |
18 | 21 | (defn ^:private parse-usage [usage] |
19 | 22 | (let [input-cache-read-tokens (-> usage :prompt_tokens_details :cached_tokens)] |
20 | 23 | {:input-tokens (if input-cache-read-tokens |
|
73 | 76 | tool-turn-id (when (seq (:tool_calls message)) (str (random-uuid))) |
74 | 77 | tools-to-call (->> (:tool_calls message) |
75 | 78 | (map-indexed (fn [idx tool-call] |
76 | | - (cond-> {:index idx |
77 | | - :id (:id tool-call) |
78 | | - :full-name (:name (:function tool-call)) |
79 | | - :arguments (json/parse-string (:arguments (:function tool-call)))} |
80 | | - tool-turn-id (assoc :tool-turn-id tool-turn-id) |
81 | | - ;; Preserve Google Gemini thought signatures |
82 | | - (get-in tool-call [:extra_content :google :thought_signature]) |
83 | | - (assoc :external-id |
84 | | - (get-in tool-call [:extra_content :google :thought_signature])))))) |
| 79 | + (let [full-name (:name (:function tool-call)) |
| 80 | + args-text (:arguments (:function tool-call))] |
| 81 | + (try |
| 82 | + (cond-> {:index idx |
| 83 | + ;; Provider-supplied tool_call_id is not safe to use as internal |
| 84 | + ;; identity (can be duplicated / contain spaces, etc). |
| 85 | + :id (new-internal-tool-call-id) |
| 86 | + :llm-tool-call-id (:id tool-call) |
| 87 | + :full-name full-name |
| 88 | + :arguments (json/parse-string args-text)} |
| 89 | + tool-turn-id (assoc :tool-turn-id tool-turn-id) |
| 90 | + ;; Preserve Google Gemini thought signatures |
| 91 | + (get-in tool-call [:extra_content :google :thought_signature]) |
| 92 | + (assoc :external-id |
| 93 | + (get-in tool-call [:extra_content :google :thought_signature]))) |
| 94 | + (catch Exception e |
| 95 | + (logger/warn logger-tag |
| 96 | + (format "Failed to parse JSON arguments for tool '%s': %s" |
| 97 | + full-name |
| 98 | + (ex-message e))) |
| 99 | + nil))))) |
| 100 | + (remove nil?) |
| 101 | + vec) |
85 | 102 | ;; DeepSeek returns reasoning_content, OpenAI o1 returns reasoning |
86 | 103 | reasoning-content (:reasoning_content message)] |
87 | 104 | {:usage (parse-usage usage) |
|
145 | 162 | - Otherwise, wrap the text in thinking tags (text-based reasoning fallback)" |
146 | 163 | [{:keys [role content] :as _msg} supports-image? think-tag-start think-tag-end] |
147 | 164 | (case role |
148 | | - "tool_call" {:role "assistant" |
149 | | - :tool_calls [(cond-> {:id (:id content) |
150 | | - :type "function" |
151 | | - :function {:name (:full-name content) |
152 | | - :arguments (json/generate-string (:arguments content))}} |
153 | | - ;; Preserve Google Gemini thought signatures if present |
154 | | - (:external-id content) |
155 | | - (assoc-in [:extra_content :google :thought_signature] |
156 | | - (:external-id content)))]} |
| 165 | + "tool_call" (let [tool-call-id (or (:llm-tool-call-id content) (:id content))] |
| 166 | + {:role "assistant" |
| 167 | + :tool_calls [(cond-> {:id tool-call-id |
| 168 | + :type "function" |
| 169 | + :function {:name (:full-name content) |
| 170 | + :arguments (json/generate-string (:arguments content))}} |
| 171 | + ;; Preserve Google Gemini thought signatures if present |
| 172 | + (:external-id content) |
| 173 | + (assoc-in [:extra_content :google :thought_signature] |
| 174 | + (:external-id content)))]}) |
157 | 175 | "tool_call_output" {:role "tool" |
158 | | - :tool_call_id (:id content) |
| 176 | + :tool_call_id (or (:llm-tool-call-id content) (:id content)) |
159 | 177 | :content (llm-util/stringfy-tool-result content)} |
160 | 178 | "user" {:role "user" |
161 | 179 | :content (extract-content content supports-image?)} |
|
474 | 492 | (on-reason {:status :started :id new-reason-id}))) |
475 | 493 | find-existing-tool-key (fn [tool-calls index id] |
476 | 494 | ;; Find existing tool call by id match, or by index when delta has no id |
477 | | - (some (fn [[k v]] (when (or (some-> id (= (:id v))) |
| 495 | + (some (fn [[k v]] (when (or (some-> id (= (:llm-tool-call-id v))) |
478 | 496 | (and (nil? id) |
479 | 497 | (some-> index (= (:index v))))) |
480 | 498 | k)) |
|
566 | 584 | ;; Providers may omit tool_calls[].index; preserve |
567 | 585 | ;; stream arrival order for a stable UX fallback. |
568 | 586 | created? (assoc :stream-order (count tool-calls)) |
| 587 | + created? (assoc :id (new-internal-tool-call-id)) |
569 | 588 | (some? index) (assoc :index index) |
570 | | - (and id (nil? (:id existing))) (assoc :id id) |
| 589 | + (and id (nil? (:llm-tool-call-id existing))) (assoc :llm-tool-call-id id) |
571 | 590 | (and name (nil? (:full-name existing))) (assoc :full-name name) |
572 | 591 | args (update :arguments-text (fnil str "") args) |
573 | 592 | ;; Store thought signature for Google Gemini |
|
0 commit comments