Skip to content

Commit 874af04

Browse files
committed
Support provider-level extraHeaders
Sent on completion and models list requests, model-level extraHeaders win on conflicts. Closes #517
1 parent cb26630 commit 874af04

8 files changed

Lines changed: 74 additions & 12 deletions

File tree

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 provider-level `extraHeaders`, sent on completion and models list requests. #517
6+
57
## 0.144.0
68

79
- Add built-in variants for `glm-5.2` (case-insensitive): `none`, `medium`, `high`.

docs/config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,14 @@
650650
"okhttp"
651651
]
652652
},
653+
"extraHeaders": {
654+
"type": "object",
655+
"description": "Additional HTTP headers to include in all requests to this provider (completions and models list fetching). Model-level extraHeaders take precedence on conflicts.",
656+
"markdownDescription": "Additional HTTP headers to include in all requests to this provider (completions and models list fetching). Model-level `extraHeaders` take precedence on conflicts.",
657+
"additionalProperties": {
658+
"type": "string"
659+
}
660+
},
653661
"retryRules": {
654662
"type": "array",
655663
"description": "Custom retry rules. Each rule can match by HTTP status code and/or error text regex pattern. When matched, the request is retried with exponential backoff.",

docs/config/models.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ Schema:
368368
| `thinkTagEnd` | string | Optional override the think end tag for openai-chat (Default: "</think>") api | No |
369369
| `httpClient` | map | Allow customize the http-client for this provider requests, like changing http version | No |
370370
| `retryRules` | array | Custom retry rules that match by HTTP status and/or error pattern (see [Retry Rules](#retry-rules)) | No |
371+
| `extraHeaders` | map | Extra headers sent on all requests to this provider (completion and models list fetch). Model-level `extraHeaders` win on conflicts | No |
371372
| `models` | map | Key: model name, value: its config | Yes |
372373
| `models <model> extraPayload` | map | Extra payload sent in body to LLM | No |
373374
| `models <model> extraHeaders` | map | Extra headers sent to LLM request | No |

src/eca/config.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,7 @@
540540
:stringfy-key
541541
[[:agent]
542542
[:providers]
543+
[:providers :ANY :extraHeaders]
543544
[:providers :ANY :models]
544545
[:providers :ANY :models :ANY :extraHeaders]
545546
[:providers :ANY :models :ANY :variants]

src/eca/llm_api.clj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@
236236
(catch Exception e
237237
(logger/warn logger-tag "on-history-sanitized callback failed" {:exception (ex-message e)}))))
238238
extra-payload (extra-payload-considering-variant model-config variant api-handler reason?)
239-
extra-headers (:extraHeaders model-config)
239+
extra-headers (merge (:extraHeaders provider-config)
240+
(:extraHeaders model-config))
240241
reasoning-history (or (:reasoningHistory model-config) :all)
241242
[auth-type api-key] (llm-util/provider-api-key provider provider-auth config)
242243
api-url (llm-util/provider-api-url provider config)

src/eca/models.clj

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,19 @@
4545
nil))
4646

4747
(defn ^:private models-endpoint-headers
48-
[provider auth-type api-type api-key]
48+
[provider auth-type api-type api-key extra-headers]
4949
(let [oauth? (= :auth/oauth auth-type)
5050
anthropic? (= "anthropic" api-type)]
5151
(client/merge-llm-headers
52-
(assoc-some
53-
(cond-> {"Content-Type" "application/json"}
54-
(= "github-copilot" provider) (merge (llm-util/copilot-ide-headers)))
55-
"anthropic-version" (when anthropic? "2023-06-01")
56-
"x-api-key" (when (and api-key anthropic? (not oauth?)) api-key)
57-
"Authorization" (when (and api-key (or oauth? (not anthropic?))) (str "Bearer " api-key))
58-
"anthropic-beta" (when (and anthropic? oauth?) "oauth-2025-04-20")))))
52+
(merge
53+
(assoc-some
54+
(cond-> {"Content-Type" "application/json"}
55+
(= "github-copilot" provider) (merge (llm-util/copilot-ide-headers)))
56+
"anthropic-version" (when anthropic? "2023-06-01")
57+
"x-api-key" (when (and api-key anthropic? (not oauth?)) api-key)
58+
"Authorization" (when (and api-key (or oauth? (not anthropic?))) (str "Bearer " api-key))
59+
"anthropic-beta" (when (and anthropic? oauth?) "oauth-2025-04-20"))
60+
extra-headers))))
5961

6062
(defn ^:private fetch-models-dev-data []
6163
(let [{:keys [status body]} (http/get models-dev-api-url
@@ -280,11 +282,11 @@
280282
(defn ^:private fetch-provider-native-models
281283
"Fetches models from provider's native /models endpoint.
282284
Returns a map of model-id -> {} on success, nil on failure."
283-
[{:keys [api-url auth-type api-key api-type provider]}]
285+
[{:keys [api-url auth-type api-key api-type provider extra-headers]}]
284286
(when-let [models-path (provider-models-endpoint-path api-type)]
285287
(let [url (shared/join-api-url api-url models-path)
286288
rid (llm-util/gen-rid)
287-
headers (models-endpoint-headers provider auth-type api-type api-key)]
289+
headers (models-endpoint-headers provider auth-type api-type api-key extra-headers)]
288290
(try
289291
(logger/debug logger-tag (format "[%s] Provider '%s': Fetching models from %s" rid provider url))
290292
(let [{:keys [status body]} (http/get url
@@ -339,7 +341,8 @@
339341
:api-url api-url
340342
:auth-type auth-type
341343
:api-key api-key
342-
:api-type api-type})))]
344+
:api-type api-type
345+
:extra-headers (:extraHeaders provider-config)})))]
343346
(logger/debug logger-tag
344347
(format "Provider '%s': Discovered %d models"
345348
provider (count models)))

test/eca/llm_api_test.clj

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,32 @@
417417
(is (= 300 (:stream-idle-timeout-seconds @captured*))
418418
"anthropic handler should receive :stream-idle-timeout-seconds from top-level config"))))
419419

420+
(deftest prompt-merges-provider-and-model-extra-headers-test
421+
(testing "provider-level extraHeaders are sent and model-level ones win on conflicts"
422+
(let [captured* (atom nil)]
423+
(with-redefs [llm-providers.openai/create-response!
424+
(fn [opts _callbacks] (reset! captured* opts) :ok)]
425+
(#'eca.llm-api/prompt!
426+
{:provider "openai"
427+
:model "gpt-5.2"
428+
:model-capabilities {:tools true
429+
:reason? false
430+
:web-search false
431+
:model-name "gpt-5.2"}
432+
:user-messages [{:role "user" :content [{:type :text :text "hi"}]}]
433+
:past-messages []
434+
:tools []
435+
:provider-auth {:api-key "test-key"}
436+
:config {:providers {"openai" {:url "https://api.openai.com"
437+
:key "test-key"
438+
:extraHeaders {"Ocp-Apim-Subscription-Key" "prov-secret"
439+
"X-Shared" "provider"}
440+
:models {"gpt-5.2" {:extraHeaders {"X-Shared" "model"}}}}}}
441+
:sync? false}))
442+
(is (= {"Ocp-Apim-Subscription-Key" "prov-secret"
443+
"X-Shared" "model"}
444+
(:extra-headers @captured*))))))
445+
420446
(deftest retry-delay-ms-test
421447
;; Formula: (quot capped 2) + rand(0, capped)
422448
;; Range: [capped/2, capped/2 + capped) = [capped/2, capped*3/2)

test/eca/models_test.clj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,26 @@
436436
(is (re-find #"^copilot-chat/" (get-in @request* [1 :headers "editor-plugin-version"])))
437437
(is (= "vscode-chat" (get-in @request* [1 :headers "copilot-integration-id"])))))))
438438

439+
(deftest fetch-provider-models-sends-provider-extra-headers-test
440+
(testing "Provider-level extraHeaders are sent on the native /models fetch"
441+
(let [request* (atom nil)]
442+
(with-redefs [http/get (fn [url opts]
443+
(reset! request* [url opts])
444+
{:status 200
445+
:body {:data [{:id "gpt-5.2"}]}})]
446+
(is (match?
447+
{"my-gateway" {"gpt-5.2" {}}}
448+
(#'models/fetch-provider-models-with-priority
449+
{:providers {"my-gateway" {:api "openai-chat"
450+
:url "https://gateway.example.com"
451+
:key "sk-test"
452+
:extraHeaders {"Ocp-Apim-Subscription-Key" "apim-secret"}}}}
453+
{}
454+
{})))
455+
(is (= "https://gateway.example.com/models" (first @request*)))
456+
(is (= "apim-secret" (get-in @request* [1 :headers "Ocp-Apim-Subscription-Key"])))
457+
(is (= "Bearer sk-test" (get-in @request* [1 :headers "Authorization"])))))))
458+
439459
(deftest fetch-provider-models-with-priority-fallback-test
440460
(let [models-dev-data {"native-provider" {"api" "https://api.openai.com"
441461
"models" {"from-models-dev" {"id" "from-models-dev"}}}}]

0 commit comments

Comments
 (0)