Skip to content

Commit 987583e

Browse files
committed
Support providers <provider> httpClient version config, allowing to use http-1.1 for some providers like lmstudio.
Fixes #229
1 parent 30e2977 commit 987583e

File tree

8 files changed

+63
-23
lines changed

8 files changed

+63
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Support `providers <provider> httpClient version` config, allowing to use http-1.1 for some providers like lmstudio. #229
6+
57
## 0.85.1
68

79
- Fix backwards compatibility for chat rollback.

docs/models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Schema:
6969
| `completionUrlRelativePath` | string | Optional override for the completion endpoint path (see defaults below and examples like Azure) | No |
7070
| `thinkTagStart` | string | Optional override the think start tag tag for openai-chat (Default: "<think>") api | No |
7171
| `thinkTagEnd` | string | Optional override the think end tag for openai-chat (Default: "</think>") api | No |
72+
| `httpClient` | map | Allow customize the http-client for this provider requests, like changing http version | No |
7273
| `models` | map | Key: model name, value: its config | Yes |
7374
| `models <model> extraPayload` | map | Extra payload sent in body to LLM | No |
7475
| `models <model> modelName` | string | Override model name, useful to have multiple models with different configs and names that use same LLM model | No |

src/eca/config.clj

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -277,12 +277,13 @@
277277
(defn ^:private normalize-fields
278278
"Converts a deep nested map where keys are strings to keywords.
279279
normalization-rules follow the nest order, :ANY means any field name.
280-
:kebab-case means convert field names to kebab-case.
281-
:stringfy means convert field names to strings."
280+
:kebab-case-key means convert field names to kebab-case.
281+
:stringfy-key means convert field names to strings."
282282
[normalization-rules m]
283-
(let [kc-paths (set (:kebab-case normalization-rules))
284-
str-paths (set (:stringfy normalization-rules))
285-
; match a current path against a rule path with :ANY wildcard
283+
(let [kc-paths (set (:kebab-case-key normalization-rules))
284+
str-paths (set (:stringfy-key normalization-rules))
285+
keywordize-paths (set (:keywordize-val normalization-rules))
286+
; match a current path against a rule path with :ANY wildcard
286287
matches-path? (fn [rule-path cur-path]
287288
(and (= (count rule-path) (count cur-path))
288289
(every? true?
@@ -295,21 +296,26 @@
295296
normalize-map (fn normalize-map [cur-path m*]
296297
(cond
297298
(map? m*)
298-
(let [apply-kebab? (applies? kc-paths cur-path)
299-
apply-string? (applies? str-paths cur-path)]
299+
(let [apply-kebab-key? (applies? kc-paths cur-path)
300+
apply-string-key? (applies? str-paths cur-path)
301+
apply-keywordize-val? (applies? keywordize-paths cur-path)
302+
]
300303
(into {}
301304
(map (fn [[k v]]
302305
(let [base-name (cond
303306
(keyword? k) (name k)
304307
(string? k) k
305308
:else (str k))
306-
kebabed (if apply-kebab?
309+
kebabed (if apply-kebab-key?
307310
(csk/->kebab-case base-name)
308311
base-name)
309-
new-k (if apply-string?
312+
new-k (if apply-string-key?
310313
kebabed
311314
(keyword kebabed))
312-
new-v (normalize-map (conj cur-path new-k) v)]
315+
new-v (if apply-keywordize-val?
316+
(keyword v)
317+
v)
318+
new-v (normalize-map (conj cur-path new-k) new-v)]
313319
[new-k new-v])))
314320
m*))
315321

@@ -320,9 +326,11 @@
320326
(normalize-map [] m)))
321327

322328
(def ^:private normalization-rules
323-
{:kebab-case
329+
{:kebab-case-key
324330
[[:providers]]
325-
:stringfy
331+
:keywordize-val
332+
[[:providers :ANY :httpClient]]
333+
:stringfy-key
326334
[[:behavior]
327335
[:providers]
328336
[:providers :ANY :models]

src/eca/llm_api.clj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@
190190
(on-error {:message (format "Unknown model %s for provider %s" (:api provider-config) provider)}))
191191
url-relative-path (:completionUrlRelativePath provider-config)
192192
think-tag-start (:thinkTagStart provider-config)
193-
think-tag-end (:thinkTagEnd provider-config)]
193+
think-tag-end (:thinkTagEnd provider-config)
194+
http-client (:httpClient provider-config)]
194195
(provider-fn
195196
{:model real-model
196197
:instructions instructions
@@ -205,6 +206,7 @@
205206
:url-relative-path url-relative-path
206207
:think-tag-start think-tag-start
207208
:think-tag-end think-tag-end
209+
:http-client http-client
208210
:api-url api-url
209211
:api-key api-key}
210212
callbacks))

src/eca/llm_providers/anthropic.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
:max_uses 10
6767
:cache_control {:type "ephemeral"}})))
6868

69-
(defn ^:private base-request! [{:keys [rid body api-url api-key auth-type url-relative-path content-block* on-error on-stream]}]
69+
(defn ^:private base-request! [{:keys [rid body api-url api-key auth-type url-relative-path content-block* on-error on-stream http-client]}]
7070
(let [url (str api-url (or url-relative-path messages-path))
7171
reason-id (str (random-uuid))
7272
oauth? (= :auth/oauth auth-type)
@@ -89,6 +89,7 @@
8989
:body (json/generate-string body)
9090
:throw-exceptions? false
9191
:async? true
92+
:http-client http-client
9293
:as (if on-stream :stream :json)}
9394
(fn [{:keys [status body]}]
9495
(try
@@ -168,7 +169,7 @@
168169
(defn chat!
169170
[{:keys [model user-messages instructions max-output-tokens
170171
api-url api-key auth-type url-relative-path reason? past-messages
171-
tools web-search extra-payload supports-image?]}
172+
tools web-search extra-payload supports-image? http-client]}
172173
{:keys [on-message-received on-error on-reason on-prepare-tool-call on-tools-called on-usage-updated] :as callbacks}]
173174
(let [messages (concat (normalize-messages past-messages supports-image?)
174175
(normalize-messages (fix-non-thinking-assistant-messages user-messages) supports-image?))
@@ -247,6 +248,7 @@
247248
:body (assoc body :messages messages)
248249
:api-url api-url
249250
:api-key api-key
251+
:http-client http-client
250252
:auth-type auth-type
251253
:url-relative-path url-relative-path
252254
:content-block* (atom nil)
@@ -265,6 +267,7 @@
265267
:body body
266268
:api-url api-url
267269
:api-key api-key
270+
:http-client http-client
268271
:auth-type auth-type
269272
:url-relative-path url-relative-path
270273
:content-block* (atom nil)

src/eca/llm_providers/openai.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
""
3333
(:content (last (:output body))))})
3434

35-
(defn ^:private base-responses-request! [{:keys [rid body api-url auth-type url-relative-path api-key on-error on-stream]}]
35+
(defn ^:private base-responses-request! [{:keys [rid body api-url auth-type url-relative-path api-key on-error on-stream http-client]}]
3636
(let [oauth? (= :auth/oauth auth-type)
3737
url (if oauth?
3838
codex-url
@@ -56,6 +56,7 @@
5656
:body (json/generate-string body)
5757
:throw-exceptions? false
5858
:async? true
59+
:http-client http-client
5960
:as (if on-stream :stream :json)}
6061
(fn [{:keys [status body]}]
6162
(try
@@ -126,7 +127,7 @@
126127
tools))
127128

128129
(defn create-response! [{:keys [model user-messages instructions reason? supports-image? api-key api-url url-relative-path
129-
max-output-tokens past-messages tools web-search extra-payload auth-type]}
130+
max-output-tokens past-messages tools web-search extra-payload auth-type http-client]}
130131
{:keys [on-message-received on-error on-prepare-tool-call on-tools-called on-reason on-usage-updated] :as callbacks}]
131132
(let [input (concat (normalize-messages past-messages supports-image?)
132133
(normalize-messages user-messages supports-image?))
@@ -239,6 +240,7 @@
239240
:api-url api-url
240241
:url-relative-path url-relative-path
241242
:api-key api-key
243+
:http-client http-client
242244
:auth-type auth-type
243245
:on-error on-error
244246
:on-stream handle-stream})
@@ -259,6 +261,7 @@
259261
:api-url api-url
260262
:url-relative-path url-relative-path
261263
:api-key api-key
264+
:http-client http-client
262265
:auth-type auth-type
263266
:on-error on-error
264267
:on-stream on-stream-fn})))

src/eca/llm_providers/openai_chat.clj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
not-empty)}))
8989

9090
(defn ^:private base-chat-request!
91-
[{:keys [rid extra-headers body url-relative-path api-url api-key on-error on-stream on-tools-called-wrapper]}]
91+
[{:keys [rid extra-headers body url-relative-path api-url api-key on-error on-stream on-tools-called-wrapper http-client]}]
9292
(let [url (str api-url (or url-relative-path chat-completions-path))
9393
on-error (if on-stream
9494
on-error
@@ -105,6 +105,7 @@
105105
:body (json/generate-string body)
106106
:throw-exceptions? false
107107
:async? true
108+
:http-client http-client
108109
:as (if on-stream :stream :json)}
109110
(fn [{:keys [status body]}]
110111
(try
@@ -294,7 +295,7 @@
294295
Compatible with OpenRouter and other OpenAI-compatible providers."
295296
[{:keys [model user-messages instructions temperature api-key api-url url-relative-path
296297
past-messages tools extra-payload extra-headers supports-image?
297-
think-tag-start think-tag-end]}
298+
think-tag-start think-tag-end http-client]}
298299
{:keys [on-message-received on-error on-prepare-tool-call on-tools-called on-reason on-usage-updated] :as callbacks}]
299300
(let [think-tag-start (or think-tag-start "<think>")
300301
think-tag-end (or think-tag-end "</think>")
@@ -340,6 +341,7 @@
340341
:body (assoc body :messages new-messages-list)
341342
:on-tools-called-wrapper on-tools-called-wrapper
342343
:extra-headers extra-headers
344+
:http-client http-client
343345
:api-url api-url
344346
:api-key api-key
345347
:url-relative-path url-relative-path
@@ -451,6 +453,7 @@
451453
:extra-headers extra-headers
452454
:api-url api-url
453455
:api-key api-key
456+
:http-client http-client
454457
:url-relative-path url-relative-path
455458
:tool-calls* tool-calls*
456459
:on-tools-called-wrapper on-tools-called-wrapper

test/eca/config_test.clj

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
:models {"gpt-5" {}}}
8585
"openrouter" {:models {"openai/o4-mini" {}}}}}
8686
(#'config/normalize-fields
87-
{:stringfy
87+
{:stringfy-key
8888
[[:providers]
8989
[:providers :ANY :models]]}
9090
{"pureConfig" true
@@ -98,15 +98,33 @@
9898
:models {"gpt-5" {}}}
9999
"open-router" {:models {"openAi/o4-mini" {}}}}}
100100
(#'config/normalize-fields
101-
{:stringfy
101+
{:stringfy-key
102102
[[:providers]
103103
[:providers :ANY :models]]
104-
:kebab-case
104+
:kebab-case-key
105105
[[:providers]]}
106106
{"pureConfig" true
107107
"providers" {"customProvider" {"key" "123"
108108
"models" {"gpt-5" {}}}
109-
"open-router" {"models" {"openAi/o4-mini" {}}}}})))))
109+
"open-router" {"models" {"openAi/o4-mini" {}}}}}))))
110+
(testing "keywordize-vals"
111+
(is (match?
112+
{:pureConfig true
113+
:providers {"custom-provider" {:key "123"
114+
:models {"gpt-5" {}}
115+
:httpClient {:version :http1.1}}}}
116+
(#'config/normalize-fields
117+
{:stringfy-key
118+
[[:providers]
119+
[:providers :ANY :models]]
120+
:kebab-case-key
121+
[[:providers]]
122+
:keywordize-val
123+
[[:providers :ANY :httpClient]]}
124+
{"pureConfig" true
125+
"providers" {"customProvider" {"key" "123"
126+
"models" {"gpt-5" {}}
127+
"httpClient" {"version" "http1.1"}}}})))))
110128

111129
(deftest validate-behavior-test
112130
(testing "valid behavior returns as-is"

0 commit comments

Comments
 (0)