Skip to content

Commit cd59656

Browse files
committed
SUpport anthropic
1 parent 772ac63 commit cd59656

File tree

3 files changed

+125
-120
lines changed

3 files changed

+125
-120
lines changed

integration-test/llm_mock/anthropic.clj

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -206,19 +206,10 @@
206206
(hk/close ch)))))
207207

208208
(defn ^:private chat-title-text-0 [ch]
209-
(sse-send! ch "content_block_delta"
210-
{:type "content_block_delta"
211-
:index 0
212-
:delta {:type "text_delta" :text "Some Cool"}})
213-
(sse-send! ch "content_block_delta"
214-
{:type "content_block_delta"
215-
:index 0
216-
:delta {:type "text_delta" :text " Title"}})
217-
(sse-send! ch "message_delta"
218-
{:type "message_delta"
219-
:delta {:stop_reason "end_turn"}
220-
:usage {:input_tokens 5
221-
:output_tokens 10}})
209+
(hk/send! ch
210+
(json/generate-string
211+
{:content [{:text "Some Cool Title"}]})
212+
true)
222213
(hk/close ch))
223214

224215
(defn handle-anthropic-messages [req]

src/eca/llm_providers/anthropic.clj

Lines changed: 119 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -65,37 +65,50 @@
6565
:max_uses 10
6666
:cache_control {:type "ephemeral"}})))
6767

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]}]
6969
(let [url (str api-url (or url-relative-path messages-path))
7070
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)))]
7278
(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*))
99112

100113
(defn ^:private normalize-messages [past-messages supports-image?]
101114
(mapv (fn [{:keys [role content] :as msg}]
@@ -152,96 +165,97 @@
152165
[{:keys [model user-messages instructions max-output-tokens
153166
api-url api-key auth-type url-relative-path reason? past-messages
154167
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}]
156169
(let [messages (concat (normalize-messages past-messages supports-image?)
157170
(normalize-messages (fix-non-thinking-assistant-messages user-messages) supports-image?))
171+
stream? (boolean callbacks)
158172
body (deep-merge
159173
(assoc-some
160174
{:model model
161175
:messages (add-cache-to-last-message messages)
162176
:max_tokens (or max-output-tokens 32000)
163-
:stream true
177+
:stream stream?
164178
:tools (->tools tools web-search)
165179
:system [{:type "text" :text "You are Claude Code, Anthropic's official CLI for Claude."}
166180
{:type "text" :text instructions :cache_control {:type "ephemeral"}}]}
167181
:thinking (when reason?
168182
{:type "enabled" :budget_tokens 2048}))
169183
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)))
170198

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)))]
245259
(base-request!
246260
{:rid (llm-util/gen-rid)
247261
:body body
@@ -251,7 +265,7 @@
251265
:url-relative-path url-relative-path
252266
:content-block* (atom nil)
253267
:on-error on-error
254-
:on-response on-response-fn})))
268+
:on-stream on-stream-fn})))
255269

256270
(def ^:private client-id "9d1c250a-e61b-44d9-88ed-5944d1962f5e")
257271

src/eca/llm_providers/openai.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
tool-call-by-item-id* (atom {})
145145
on-stream-fn
146146
(when stream?
147-
(fn handle-response [event data]
147+
(fn handle-stream [event data]
148148
(case event
149149
;; text
150150
"response.output_text.delta"
@@ -230,7 +230,7 @@
230230
:api-key api-key
231231
:auth-type auth-type
232232
:on-error on-error
233-
:on-stream handle-response})
233+
:on-stream handle-stream})
234234
(doseq [tool-call tool-calls]
235235
(swap! tool-call-by-item-id* dissoc (:item-id tool-call))))
236236
(on-message-received {:type :finish

0 commit comments

Comments
 (0)