Skip to content

Commit 81ffb65

Browse files
ericdalloeca-agent
andcommitted
Fix 400 on Copilot Claude adaptive-thinking models without variant
Copilot /models discovery routes Claude adaptive models (Opus 4.7+, Sonnet 5, etc) to the Messages API but its discovered variants replaced the built-in ones that carried the "default" adaptive variant, so no-variant requests fell back to thinking.type "enabled" which these models reject. Emit a "default" adaptive variant during discovery. Fixes #528 🤖 Generated with [ECA](https://eca.dev) (nubank-anthropic/fable-5 - max) Co-Authored-By: eca-agent <git@eca.dev>
1 parent 8d20b5f commit 81ffb65

4 files changed

Lines changed: 68 additions & 29 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+
- Fix 400 on GitHub Copilot Claude adaptive-thinking models (e.g. Opus 4.8) when no variant is selected, by defaulting to adaptive thinking. #528
6+
57
## 0.146.1
68

79
- Wait for rate-limit reset from response headers (Anthropic, OpenAI-compatible) on 429, showing reset time in chat; optional provider config `rateLimitMaxWaitSeconds` caps the wait.

src/eca/models.clj

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -315,32 +315,39 @@
315315
efforts))
316316

317317
:anthropic
318-
(cond
319-
(and efforts adaptive?)
320-
(into {}
321-
(map (fn [effort]
322-
[effort {:thinking (cond-> {:type "adaptive"}
323-
;; Opus 4.7 defaults adaptive thinking display to "omitted", which
324-
;; produces empty thinking blocks. Request summaries explicitly.
325-
(re-find #"(?i)opus[-._]4[-._]7(?:$|[-._])" model-id)
326-
(assoc :display "summarized"))
327-
:output_config {:effort effort}}]))
328-
efforts)
329-
330-
;; Some Copilot Messages models (for example Claude Opus/Sonnet 4.6) advertise
331-
;; budget bounds instead of named reasoning efforts. Project that range into
332-
;; the existing variant picker without exposing raw token budgets in the UI.
333-
(and (number? max-budget) (pos? max-budget))
334-
(let [min-value (max 1024 (long (if (number? min-budget) min-budget 1024)))
335-
max-value (dec (long max-budget))
336-
high-value (max min-value (quot (long max-budget) 2))]
337-
(when (<= min-value max-value)
338-
{"high" {:thinking {:type "enabled"
339-
:budget_tokens high-value}}
340-
"max" {:thinking {:type "enabled"
341-
:budget_tokens max-value}}}))
342-
343-
:else nil)
318+
(let [adaptive-thinking (cond-> {:type "adaptive"}
319+
;; Opus 4.7 defaults adaptive thinking display to "omitted", which
320+
;; produces empty thinking blocks. Request summaries explicitly.
321+
(re-find #"(?i)opus[-._]4[-._]7(?:$|[-._])" model-id)
322+
(assoc :display "summarized"))]
323+
(cond
324+
;; Adaptive-thinking models (Opus 4.7+/Sonnet 5+) reject thinking.type
325+
;; "enabled", so a "default" variant keeps no-variant requests on
326+
;; adaptive instead of the enabled fallback in the provider (#528).
327+
(and efforts adaptive?)
328+
(into {"default" {:thinking adaptive-thinking}}
329+
(map (fn [effort]
330+
[effort {:thinking adaptive-thinking
331+
:output_config {:effort effort}}]))
332+
efforts)
333+
334+
adaptive?
335+
{"default" {:thinking adaptive-thinking}}
336+
337+
;; Some Copilot Messages models (for example Claude Opus/Sonnet 4.5) advertise
338+
;; budget bounds instead of named reasoning efforts. Project that range into
339+
;; the existing variant picker without exposing raw token budgets in the UI.
340+
(and (number? max-budget) (pos? max-budget))
341+
(let [min-value (max 1024 (long (if (number? min-budget) min-budget 1024)))
342+
max-value (dec (long max-budget))
343+
high-value (max min-value (quot (long max-budget) 2))]
344+
(when (<= min-value max-value)
345+
{"high" {:thinking {:type "enabled"
346+
:budget_tokens high-value}}
347+
"max" {:thinking {:type "enabled"
348+
:budget_tokens max-value}}}))
349+
350+
:else nil))
344351

345352
nil)))
346353

test/eca/llm_api_test.clj

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,26 @@
354354
(is (= {:effort "high"} (get-in @captured* [:extra-payload :output_config])))
355355
(let [headers ((:extra-headers @captured*) {:body {:messages [{:role "user"}]}})]
356356
(is (= "interleaved-thinking-2025-05-14" (get headers "anthropic-beta")))
357-
(is (= "user" (get headers "x-initiator"))))))))
357+
(is (= "user" (get headers "x-initiator"))))))
358+
359+
(testing "Messages models with no selected variant fall back to discovered default (#528)"
360+
(let [captured* (atom nil)]
361+
(with-redefs [llm-providers.anthropic/chat!
362+
(fn [opts _callbacks] (reset! captured* opts) :ok)]
363+
(#'eca.llm-api/prompt!
364+
(assoc base-opts
365+
:variant nil
366+
:model "claude-adaptive"
367+
:model-capabilities {:api :anthropic
368+
:tools true
369+
:reason? true
370+
:web-search false
371+
:model-name "claude-adaptive"
372+
:variants {"default" {:thinking {:type "adaptive"}}
373+
"high" {:thinking {:type "adaptive"}
374+
:output_config {:effort "high"}}}})))
375+
(is (= {:type "adaptive"} (get-in @captured* [:extra-payload :thinking])))
376+
(is (nil? (get-in @captured* [:extra-payload :output_config])))))))
358377

359378
(deftest prompt-passes-image-generation-to-openai-handler-test
360379
(testing "openai branch forwards :image-generation true to create-response! when capability is on"

test/eca/models_test.clj

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
(:require
33
[clojure.test :refer [deftest is testing]]
44
[hato.client :as http]
5+
[matcher-combinators.matchers :as m]
56
[matcher-combinators.test :refer [match?]]
67
[eca.llm-util :as llm-util]
78
[eca.logger :as logger]
@@ -162,6 +163,10 @@
162163
:supported_endpoints ["/v1/messages"]
163164
:capabilities {:supports {:adaptive_thinking true
164165
:reasoning_effort ["high"]}}}
166+
{:id "claude-adaptive-budget"
167+
:supported_endpoints ["/v1/messages"]
168+
:capabilities {:supports {:adaptive_thinking true
169+
:max_thinking_budget 10000}}}
165170
{:id "claude-budget"
166171
:supported_endpoints ["/v1/messages"]
167172
:capabilities {:supports {:max_thinking_budget 10000}}}
@@ -186,15 +191,21 @@
186191
"claude-adaptive" {:discovered-api :anthropic
187192
:discovered-reason? true
188193
:discovered-variants
189-
{"low" {:thinking {:type "adaptive"}
194+
{"default" {:thinking {:type "adaptive"}}
195+
"low" {:thinking {:type "adaptive"}
190196
:output_config {:effort "low"}}
191197
"high" {:thinking {:type "adaptive"}
192198
:output_config {:effort "high"}}}}
193199
"claude-opus-4-7" {:discovered-api :anthropic
194200
:discovered-reason? true
195201
:discovered-variants
196-
{"high" {:thinking {:type "adaptive" :display "summarized"}
202+
{"default" {:thinking {:type "adaptive" :display "summarized"}}
203+
"high" {:thinking {:type "adaptive" :display "summarized"}
197204
:output_config {:effort "high"}}}}
205+
"claude-adaptive-budget" {:discovered-api :anthropic
206+
:discovered-reason? true
207+
:discovered-variants
208+
(m/equals {"default" {:thinking {:type "adaptive"}}})}
198209
"claude-budget" {:discovered-api :anthropic
199210
:discovered-reason? true
200211
:discovered-variants

0 commit comments

Comments
 (0)