|
65 | 65 | :max_uses 10 |
66 | 66 | :cache_control {:type "ephemeral"}}))) |
67 | 67 |
|
68 | | -(defn ^:private base-request! [{:keys [rid body api-url api-key auth-type url-relative-path content-block* on-error on-response]}] |
| 68 | +(defn ^:private base-request! [{:keys [rid body api-url api-key auth-type url-relative-path content-block* on-error on-stream]}] |
69 | 69 | (let [url (str api-url (or url-relative-path messages-path)) |
70 | 70 | reason-id (str (random-uuid)) |
71 | | - oauth? (= :auth/oauth auth-type)] |
| 71 | + oauth? (= :auth/oauth auth-type) |
| 72 | + response* (atom nil) |
| 73 | + on-error (if on-stream |
| 74 | + on-error |
| 75 | + (fn [error-data] |
| 76 | + (llm-util/log-response logger-tag rid "response-error" body) |
| 77 | + (reset! response* error-data)))] |
72 | 78 | (llm-util/log-request logger-tag rid url body) |
73 | | - (http/post |
74 | | - url |
75 | | - {:headers (assoc-some |
76 | | - {"anthropic-version" "2023-06-01" |
77 | | - "Content-Type" "application/json"} |
78 | | - "x-api-key" (when-not oauth? api-key) |
79 | | - "Authorization" (when oauth? (str "Bearer " api-key)) |
80 | | - "anthropic-beta" (when oauth? "oauth-2025-04-20")) |
81 | | - :body (json/generate-string body) |
82 | | - :throw-exceptions? false |
83 | | - :async? true |
84 | | - :as :stream} |
85 | | - (fn [{:keys [status body]}] |
86 | | - (try |
87 | | - (if (not= 200 status) |
88 | | - (let [body-str (slurp body)] |
89 | | - (logger/warn logger-tag "Unexpected response status: %s body: %s" status body-str) |
90 | | - (on-error {:message (format "Anthropic response status: %s body: %s" status body-str)})) |
91 | | - (with-open [rdr (io/reader body)] |
92 | | - (doseq [[event data] (llm-util/event-data-seq rdr)] |
93 | | - (llm-util/log-response logger-tag rid event data) |
94 | | - (on-response event data content-block* reason-id)))) |
95 | | - (catch Exception e |
96 | | - (on-error {:exception e})))) |
97 | | - (fn [e] |
98 | | - (on-error {:exception e}))))) |
| 79 | + @(http/post |
| 80 | + url |
| 81 | + {:headers (assoc-some |
| 82 | + {"anthropic-version" "2023-06-01" |
| 83 | + "Content-Type" "application/json"} |
| 84 | + "x-api-key" (when-not oauth? api-key) |
| 85 | + "Authorization" (when oauth? (str "Bearer " api-key)) |
| 86 | + "anthropic-beta" (when oauth? "oauth-2025-04-20")) |
| 87 | + :body (json/generate-string body) |
| 88 | + :throw-exceptions? false |
| 89 | + :async? true |
| 90 | + :as (if on-stream :stream :json)} |
| 91 | + (fn [{:keys [status body]}] |
| 92 | + (try |
| 93 | + (if (not= 200 status) |
| 94 | + (let [body-str (if on-stream (slurp body) body)] |
| 95 | + (logger/warn logger-tag "Unexpected response status: %s body: %s" status body-str) |
| 96 | + (on-error {:message (format "Anthropic response status: %s body: %s" status body-str)})) |
| 97 | + (if on-stream |
| 98 | + (with-open [rdr (io/reader body)] |
| 99 | + (doseq [[event data] (llm-util/event-data-seq rdr)] |
| 100 | + (llm-util/log-response logger-tag rid event data) |
| 101 | + (on-stream event data content-block* reason-id))) |
| 102 | + |
| 103 | + (do |
| 104 | + (llm-util/log-response logger-tag rid "response" body) |
| 105 | + (reset! response* |
| 106 | + {:result (:text (last (:content body)))})))) |
| 107 | + (catch Exception e |
| 108 | + (on-error {:exception e})))) |
| 109 | + (fn [e] |
| 110 | + (on-error {:exception e}))) |
| 111 | + @response*)) |
99 | 112 |
|
100 | 113 | (defn ^:private normalize-messages [past-messages supports-image?] |
101 | 114 | (mapv (fn [{:keys [role content] :as msg}] |
|
152 | 165 | [{:keys [model user-messages instructions max-output-tokens |
153 | 166 | api-url api-key auth-type url-relative-path reason? past-messages |
154 | 167 | tools web-search extra-payload supports-image?]} |
155 | | - {:keys [on-message-received on-error on-reason on-prepare-tool-call on-tools-called on-usage-updated]}] |
| 168 | + {:keys [on-message-received on-error on-reason on-prepare-tool-call on-tools-called on-usage-updated] :as callbacks}] |
156 | 169 | (let [messages (concat (normalize-messages past-messages supports-image?) |
157 | 170 | (normalize-messages (fix-non-thinking-assistant-messages user-messages) supports-image?)) |
| 171 | + stream? (boolean callbacks) |
158 | 172 | body (deep-merge |
159 | 173 | (assoc-some |
160 | 174 | {:model model |
161 | 175 | :messages (add-cache-to-last-message messages) |
162 | 176 | :max_tokens (or max-output-tokens 32000) |
163 | | - :stream true |
| 177 | + :stream stream? |
164 | 178 | :tools (->tools tools web-search) |
165 | 179 | :system [{:type "text" :text "You are Claude Code, Anthropic's official CLI for Claude."} |
166 | 180 | {:type "text" :text instructions :cache_control {:type "ephemeral"}}]} |
167 | 181 | :thinking (when reason? |
168 | 182 | {:type "enabled" :budget_tokens 2048})) |
169 | 183 | extra-payload) |
| 184 | + on-stream-fn |
| 185 | + (when stream? |
| 186 | + (fn handle-stream [event data content-block* reason-id] |
| 187 | + (case event |
| 188 | + "content_block_start" (case (-> data :content_block :type) |
| 189 | + "thinking" (do |
| 190 | + (on-reason {:status :started |
| 191 | + :id reason-id}) |
| 192 | + (swap! content-block* assoc (:index data) (:content_block data))) |
| 193 | + "tool_use" (do |
| 194 | + (on-prepare-tool-call {:name (-> data :content_block :name) |
| 195 | + :id (-> data :content_block :id) |
| 196 | + :arguments-text ""}) |
| 197 | + (swap! content-block* assoc (:index data) (:content_block data))) |
170 | 198 |
|
171 | | - on-response-fn |
172 | | - (fn handle-response [event data content-block* reason-id] |
173 | | - (case event |
174 | | - "content_block_start" (case (-> data :content_block :type) |
175 | | - "thinking" (do |
176 | | - (on-reason {:status :started |
177 | | - :id reason-id}) |
178 | | - (swap! content-block* assoc (:index data) (:content_block data))) |
179 | | - "tool_use" (do |
180 | | - (on-prepare-tool-call {:name (-> data :content_block :name) |
181 | | - :id (-> data :content_block :id) |
182 | | - :arguments-text ""}) |
183 | | - (swap! content-block* assoc (:index data) (:content_block data))) |
184 | | - |
185 | | - nil) |
186 | | - "content_block_delta" (case (-> data :delta :type) |
187 | | - "text_delta" (on-message-received {:type :text |
188 | | - :text (-> data :delta :text)}) |
189 | | - "input_json_delta" (let [text (-> data :delta :partial_json) |
190 | | - _ (swap! content-block* update-in [(:index data) :input-json] str text) |
191 | | - content-block (get @content-block* (:index data))] |
192 | | - (on-prepare-tool-call {:name (:name content-block) |
193 | | - :id (:id content-block) |
194 | | - :arguments-text text})) |
195 | | - "citations_delta" (case (-> data :delta :citation :type) |
196 | | - "web_search_result_location" (on-message-received |
197 | | - {:type :url |
198 | | - :title (-> data :delta :citation :title) |
199 | | - :url (-> data :delta :citation :url)}) |
200 | | - nil) |
201 | | - "thinking_delta" (on-reason {:status :thinking |
202 | | - :id reason-id |
203 | | - :text (-> data :delta :thinking)}) |
204 | | - "signature_delta" (on-reason {:status :finished |
205 | | - :external-id (-> data :delta :signature) |
206 | | - :id reason-id}) |
207 | | - nil) |
208 | | - "message_delta" (do |
209 | | - (when-let [usage (and (-> data :delta :stop_reason) |
210 | | - (:usage data))] |
211 | | - (on-usage-updated {:input-tokens (:input_tokens usage) |
212 | | - :input-cache-creation-tokens (:cache_creation_input_tokens usage) |
213 | | - :input-cache-read-tokens (:cache_read_input_tokens usage) |
214 | | - :output-tokens (:output_tokens usage)})) |
215 | | - (case (-> data :delta :stop_reason) |
216 | | - "tool_use" (let [tool-calls (keep |
217 | | - (fn [content-block] |
218 | | - (when (= "tool_use" (:type content-block)) |
219 | | - {:id (:id content-block) |
220 | | - :name (:name content-block) |
221 | | - :arguments (json/parse-string (:input-json content-block))})) |
222 | | - (vals @content-block*))] |
223 | | - (when-let [{:keys [new-messages]} (on-tools-called tool-calls)] |
224 | | - (let [messages (-> (normalize-messages new-messages supports-image?) |
225 | | - add-cache-to-last-message)] |
226 | | - (reset! content-block* {}) |
227 | | - (base-request! |
228 | | - {:rid (llm-util/gen-rid) |
229 | | - :body (assoc body :messages messages) |
230 | | - :api-url api-url |
231 | | - :api-key api-key |
232 | | - :auth-type auth-type |
233 | | - :url-relative-path url-relative-path |
234 | | - :content-block* (atom nil) |
235 | | - :on-error on-error |
236 | | - :on-response handle-response})))) |
237 | | - "end_turn" (do |
238 | | - (reset! content-block* {}) |
239 | | - (on-message-received {:type :finish |
240 | | - :finish-reason (-> data :delta :stop_reason)})) |
241 | | - "max_tokens" (on-message-received {:type :limit-reached |
242 | | - :tokens (:usage data)}) |
243 | | - nil)) |
244 | | - nil))] |
| 199 | + nil) |
| 200 | + "content_block_delta" (case (-> data :delta :type) |
| 201 | + "text_delta" (on-message-received {:type :text |
| 202 | + :text (-> data :delta :text)}) |
| 203 | + "input_json_delta" (let [text (-> data :delta :partial_json) |
| 204 | + _ (swap! content-block* update-in [(:index data) :input-json] str text) |
| 205 | + content-block (get @content-block* (:index data))] |
| 206 | + (on-prepare-tool-call {:name (:name content-block) |
| 207 | + :id (:id content-block) |
| 208 | + :arguments-text text})) |
| 209 | + "citations_delta" (case (-> data :delta :citation :type) |
| 210 | + "web_search_result_location" (on-message-received |
| 211 | + {:type :url |
| 212 | + :title (-> data :delta :citation :title) |
| 213 | + :url (-> data :delta :citation :url)}) |
| 214 | + nil) |
| 215 | + "thinking_delta" (on-reason {:status :thinking |
| 216 | + :id reason-id |
| 217 | + :text (-> data :delta :thinking)}) |
| 218 | + "signature_delta" (on-reason {:status :finished |
| 219 | + :external-id (-> data :delta :signature) |
| 220 | + :id reason-id}) |
| 221 | + nil) |
| 222 | + "message_delta" (do |
| 223 | + (when-let [usage (and (-> data :delta :stop_reason) |
| 224 | + (:usage data))] |
| 225 | + (on-usage-updated {:input-tokens (:input_tokens usage) |
| 226 | + :input-cache-creation-tokens (:cache_creation_input_tokens usage) |
| 227 | + :input-cache-read-tokens (:cache_read_input_tokens usage) |
| 228 | + :output-tokens (:output_tokens usage)})) |
| 229 | + (case (-> data :delta :stop_reason) |
| 230 | + "tool_use" (let [tool-calls (keep |
| 231 | + (fn [content-block] |
| 232 | + (when (= "tool_use" (:type content-block)) |
| 233 | + {:id (:id content-block) |
| 234 | + :name (:name content-block) |
| 235 | + :arguments (json/parse-string (:input-json content-block))})) |
| 236 | + (vals @content-block*))] |
| 237 | + (when-let [{:keys [new-messages]} (on-tools-called tool-calls)] |
| 238 | + (let [messages (-> (normalize-messages new-messages supports-image?) |
| 239 | + add-cache-to-last-message)] |
| 240 | + (reset! content-block* {}) |
| 241 | + (base-request! |
| 242 | + {:rid (llm-util/gen-rid) |
| 243 | + :body (assoc body :messages messages) |
| 244 | + :api-url api-url |
| 245 | + :api-key api-key |
| 246 | + :auth-type auth-type |
| 247 | + :url-relative-path url-relative-path |
| 248 | + :content-block* (atom nil) |
| 249 | + :on-error on-error |
| 250 | + :on-stream handle-stream})))) |
| 251 | + "end_turn" (do |
| 252 | + (reset! content-block* {}) |
| 253 | + (on-message-received {:type :finish |
| 254 | + :finish-reason (-> data :delta :stop_reason)})) |
| 255 | + "max_tokens" (on-message-received {:type :limit-reached |
| 256 | + :tokens (:usage data)}) |
| 257 | + nil)) |
| 258 | + nil)))] |
245 | 259 | (base-request! |
246 | 260 | {:rid (llm-util/gen-rid) |
247 | 261 | :body body |
|
251 | 265 | :url-relative-path url-relative-path |
252 | 266 | :content-block* (atom nil) |
253 | 267 | :on-error on-error |
254 | | - :on-response on-response-fn}))) |
| 268 | + :on-stream on-stream-fn}))) |
255 | 269 |
|
256 | 270 | (def ^:private client-id "9d1c250a-e61b-44d9-88ed-5944d1962f5e") |
257 | 271 |
|
|
0 commit comments