From b6579cbdad163df081076fa3e9e234828e722670 Mon Sep 17 00:00:00 2001 From: Adam Bobrow Date: Wed, 25 Feb 2026 23:24:56 +0200 Subject: [PATCH] Support Claude console subscription auth for model listing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use configurable auth type from provider config (defaulting to :auth/oauth) so that token-based and OAuth-based providers (e.g. Claude console subscription) send the correct header (x-api-key vs Authorization: Bearer) when fetching models. 🤖 Generated with [eca](https://eca.dev) Co-Authored-By: eca --- CHANGELOG.md | 1 + src/eca/llm_util.clj | 2 +- src/eca/models.clj | 24 ++++++++++++++---------- test/eca/models_test.clj | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8c8d2e46..314c938e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Improve `editor_diagnostics` tool summary to show the target filename (e.g. `Checking diagnostics: foo.clj`) or `Checking all diagnostics` when no path is provided. - Bugfix: preserve the chat's selected variant when changing model on an existing chat (regression from per-chat scoping in v0.133.1). `chat/selectedModelChanged` now keeps the chat's persisted `:variant` if it is still supported by the new model, before falling back to the chat's agent variant. Resume flows (`chat/open`, `/resume`) align the same way. - Native ECA tools now auto-resolve bare tool names like `write_file` to their canonical `eca__...` form, avoiding repeated failed retries when an LLM omits the native server prefix. +- Support Claude console subscription auth: dispatch `x-api-key` or `Authorization: Bearer` header based on configurable auth type. ## 0.133.2 diff --git a/src/eca/llm_util.clj b/src/eca/llm_util.clj index b175a701b..3db1e5357 100644 --- a/src/eca/llm_util.clj +++ b/src/eca/llm_util.clj @@ -139,7 +139,7 @@ (or (when-let [key (not-empty (get-in config [:providers (name provider) :key]))] [:auth/token key]) (when-let [key (:api-key provider-auth)] - [:auth/oauth key]) + [(get provider-auth :type :auth/oauth) key]) (when-let [key (config/get-env (str (csk/->SCREAMING_SNAKE_CASE (name provider)) "_API_KEY"))] [:auth/token key]) ;; legacy diff --git a/src/eca/models.clj b/src/eca/models.clj index dcc5fb105..34c85b998 100644 --- a/src/eca/models.clj +++ b/src/eca/models.clj @@ -31,11 +31,14 @@ nil)) (defn ^:private models-endpoint-headers - [api-key] - (client/merge-llm-headers - (assoc-some - {"Content-Type" "application/json"} - "Authorization" (when api-key (str "Bearer " api-key))))) + [auth-type api-type api-key] + (let [oauth? (= :auth/oauth auth-type) + anthropic? (= "anthropic" api-type)] + (client/merge-llm-headers + (assoc-some + {"Content-Type" "application/json"} + "x-api-key" (when (and api-key anthropic? (not oauth?)) api-key) + "Authorization" (when (and api-key (or oauth? (not anthropic?))) (str "Bearer " api-key)))))) (defn ^:private fetch-models-dev-data [] (let [{:keys [status body]} (http/get models-dev-api-url @@ -235,11 +238,11 @@ (defn ^:private fetch-provider-native-models "Fetches models from provider's native /models endpoint. Returns a map of model-id -> {} on success, nil on failure." - [{:keys [api-url api-key api-type provider]}] + [{:keys [api-url auth-type api-key api-type provider]}] (when-let [models-path (provider-models-endpoint-path api-type)] (let [url (shared/join-api-url api-url models-path) rid (llm-util/gen-rid) - headers (models-endpoint-headers api-key)] + headers (models-endpoint-headers auth-type api-type api-key)] (try (logger/debug logger-tag (format "[%s] Provider '%s': Fetching models from %s" rid provider url)) (let [{:keys [status body]} (http/get url @@ -275,14 +278,15 @@ (when (contains? native-models-endpoint-providers (:api provider-config)) (let [api-url (or (get-in db [:auth provider :api-url]) (llm-util/provider-api-url provider config)) - [_ api-key] (llm-util/provider-api-key provider - (get-in db [:auth provider]) - config) + [auth-type api-key] (llm-util/provider-api-key provider + (get-in db [:auth provider]) + config) api-type (:api provider-config)] (when api-url (when-let [models (fetch-provider-native-models {:provider provider :api-url api-url + :auth-type auth-type :api-key api-key :api-type api-type})] (logger/info logger-tag diff --git a/test/eca/models_test.clj b/test/eca/models_test.clj index 697ee7fcb..91caf2e56 100644 --- a/test/eca/models_test.clj +++ b/test/eca/models_test.clj @@ -268,6 +268,39 @@ (is (= "https://api.openai.com/v1/models" (first @request*))) (is (= "Bearer sk-test" (get-in @request* [1 :headers "Authorization"])))))) +(deftest fetch-provider-models-anthropic-token-uses-x-api-key-test + (let [request* (atom nil) + models-dev-data {"anthropic" {"api" "https://api.anthropic.com" + "models" {"from-models-dev" {"id" "from-models-dev"}}}}] + (with-redefs [http/get (fn [url opts] + (reset! request* [url opts]) + {:status 200 + :body {:data [{:id "claude-3"}]}})] + (#'models/fetch-provider-models-with-priority + {:providers {"anthropic" {:api "anthropic" + :url "https://api.anthropic.com" + :key "sk-ant-test"}}} + {} + models-dev-data) + (is (= "sk-ant-test" (get-in @request* [1 :headers "x-api-key"]))) + (is (nil? (get-in @request* [1 :headers "Authorization"])))))) + +(deftest fetch-provider-models-anthropic-oauth-uses-bearer-test + (let [request* (atom nil) + models-dev-data {"anthropic" {"api" "https://api.anthropic.com" + "models" {"from-models-dev" {"id" "from-models-dev"}}}}] + (with-redefs [http/get (fn [url opts] + (reset! request* [url opts]) + {:status 200 + :body {:data [{:id "claude-3"}]}})] + (#'models/fetch-provider-models-with-priority + {:providers {"anthropic" {:api "anthropic" + :url "https://api.anthropic.com"}}} + {:auth {"anthropic" {:api-key "oauth-token" :type :auth/oauth}}} + models-dev-data) + (is (= "Bearer oauth-token" (get-in @request* [1 :headers "Authorization"]))) + (is (nil? (get-in @request* [1 :headers "x-api-key"])))))) + (deftest fetch-provider-models-with-priority-fallback-test (let [models-dev-data {"native-provider" {"api" "https://api.openai.com" "models" {"from-models-dev" {"id" "from-models-dev"}}}}]