Skip to content

Issue calling Claude 4.7+ through bedrock #502

@JeremyWeed

Description

@JeremyWeed

To upvote this issue, give it a thumbs up. See this list for the most upvoted issues.

Please avoid AI slops, be concise, and focus on what matters for this issue.

Describe the bug

Getting this error only on 4.7+:

Bedrock response status: 400 body: {"message":"The model returned the following errors: \"thinking.type.enabled\" is not supported for this model. Use \"thinking.type.adaptive\" and \"output_config.effort\" to control thinking behavior."}

Believe it's due to this section where 'enabled' is hardcoded: https://github.com/editor-code-assistant/eca/blame/b986a52a7c73b6bddcbc190fb70ea1bf5b6cc7be/src/eca/llm_providers/bedrock.clj#L162

<=4.6 all work as expected.

To Reproduce

Steps to reproduce the behavior:
Configure ECA with this config file:

{
  "providers": {
    "bedrock": {
      "api": "bedrock",
      "url": "https://bedrock-runtime.us-east-1.amazonaws.com",
      "key": "...",
      "models": {
        "us.anthropic.claude-opus-4-6-v1": {},
        "us.anthropic.claude-opus-4-7": {},
        "us.anthropic.claude-opus-4-8": {},
        "us.anthropic.claude-opus-4-5-20251101-v1:0": {},
        "us.anthropic.claude-sonnet-4-5-20250929-v1:0": {},
        "us.anthropic.claude-haiku-4-5-20251001-v1:0": {}
      }
    }
  }
}

Expected behavior

4.7+ should work like they do in the default Anthropic API integration section, with different possible variants:

eca/src/eca/config.clj

Lines 66 to 78 in b986a52

(def ^:private anthropic-variants
{"low" {:output_config {:effort "low"} :thinking {:type "adaptive"}}
"medium" {:output_config {:effort "medium"} :thinking {:type "adaptive"}}
"high" {:output_config {:effort "high"} :thinking {:type "adaptive"}}
"max" {:output_config {:effort "max"} :thinking {:type "adaptive"}}})
(def ^:private anthropic-v2-variants
{"default" {:thinking {:type "adaptive" :display "summarized"}}
"low" {:output_config {:effort "low"} :thinking {:type "adaptive" :display "summarized"}}
"medium" {:output_config {:effort "medium"} :thinking {:type "adaptive" :display "summarized"}}
"high" {:output_config {:effort "high"} :thinking {:type "adaptive" :display "summarized"}}
"xhigh" {:output_config {:effort "xhigh"} :thinking {:type "adaptive" :display "summarized"}}
"max" {:output_config {:effort "max"} :thinking {:type "adaptive" :display "summarized"}}})

I was able to bypass this failure with a small change to how config resolution works for Bedrock, and an updated config, but In don't know if this is the correct way forwards here (it'd be great to be able to select different effort amounts in the UI):

modified   src/eca/llm_providers/bedrock.clj
@@ -152,22 +152,31 @@
 
 (defn ^:private build-body
   [{:keys [messages instructions max-output-tokens tools reason? extra-payload]}]
-  (shared/deep-merge
-   (assoc-some
-    {:messages messages
-     :inferenceConfig {:maxTokens (or max-output-tokens default-max-output-tokens)}}
-    :system (when-not (string/blank? instructions) [{:text instructions}])
-    :toolConfig (->tool-config tools)
-    :additionalModelRequestFields (when reason?
-                                    {:reasoning_config {:type "enabled"
-                                                        :budget_tokens default-reasoning-budget-tokens}}))
-   ;; Drop a reasoning_config smuggled through extraPayload when the caller
-   ;; didn't request reasoning — otherwise it would silently re-enable it,
-   ;; since the generic reasoning-key strip in llm-api doesn't cover Bedrock.
-   (cond-> (select-keys extra-payload allowed-extra-payload-keys)
-     (and (not reason?)
-          (get-in extra-payload [:additionalModelRequestFields :reasoning_config]))
-     (update :additionalModelRequestFields dissoc :reasoning_config))))
+  (let [filtered-extra (cond-> (select-keys extra-payload allowed-extra-payload-keys)
+                         (and (not reason?)
+                              (get-in extra-payload [:additionalModelRequestFields :reasoning_config]))
+                         (update :additionalModelRequestFields dissoc :reasoning_config))
+        ;; When extra-payload supplies its own reasoning_config, use it as-is
+        ;; rather than deep-merging with the default. Models disagree on which
+        ;; keys are valid (e.g. "adaptive" rejects budget_tokens).
+        extra-reasoning (get-in filtered-extra [:additionalModelRequestFields :reasoning_config])
+        default-reasoning (when reason?
+                            {:type "enabled"
+                             :budget_tokens default-reasoning-budget-tokens})
+        reasoning-config (if extra-reasoning
+                           extra-reasoning
+                           default-reasoning)]
+    (shared/deep-merge
+     (assoc-some
+      {:messages messages
+       :inferenceConfig {:maxTokens (or max-output-tokens default-max-output-tokens)}}
+      :system (when-not (string/blank? instructions) [{:text instructions}])
+      :toolConfig (->tool-config tools)
+      :additionalModelRequestFields (when reasoning-config
+                                      {:reasoning_config reasoning-config}))
+     (cond-> filtered-extra
+       extra-reasoning
+       (update :additionalModelRequestFields dissoc :reasoning_config)))))
 
 ;; --- AWS event-stream (vnd.amazon.eventstream) binary decoder ---
 
modified   test/eca/llm_providers/bedrock_test.clj
@@ -124,7 +124,8 @@
                   body))))
 
   (testing "reasoning adds reasoning_config to additionalModelRequestFields"
-    (is (match? {:additionalModelRequestFields {:reasoning_config {:type "enabled"}}}
+    (is (match? {:additionalModelRequestFields {:reasoning_config {:type "enabled"
+                                                                    :budget_tokens 2048}}}
                 (#'llm-providers.bedrock/build-body {:messages [] :reason? true}))))
 
   (testing "extra-payload cannot re-enable reasoning when reason? is false"
@@ -137,14 +138,14 @@
       (is (nil? (get-in body [:additionalModelRequestFields :reasoning_config])))
       (is (= "bar" (get-in body [:additionalModelRequestFields :foo])))))
 
-  (testing "extra-payload reasoning_config is preserved when reason? is true"
-    (is (match? {:additionalModelRequestFields {:reasoning_config {:budget_tokens 1024}}}
-                (#'llm-providers.bedrock/build-body
-                 {:messages []
-                  :reason? true
-                  :extra-payload {:additionalModelRequestFields
-                                  {:reasoning_config {:budget_tokens 1024}}}})))))
-
+  (testing "extra-payload reasoning_config fully replaces default when reason? is true"
+    (let [body (#'llm-providers.bedrock/build-body
+                {:messages []
+                 :reason? true
+                 :extra-payload {:additionalModelRequestFields
+                                 {:reasoning_config {:type "adaptive"}}}})]
+      (is (= {:type "adaptive"}
+             (get-in body [:additionalModelRequestFields :reasoning_config]))))))
 ;; --- chat! non-streaming ---
 
 (deftest chat!-non-streaming-test

and config.json model section:

      "models": {
        "us.anthropic.claude-opus-4-6-v1": {},
        "us.anthropic.claude-opus-4-7": {
          "extraPayload": {
            "additionalModelRequestFields": {
              "reasoning_config": {
                "type": "adaptive"
              },
              "output_config": { "effort": "high" }
            }
          }
        },
        "us.anthropic.claude-opus-4-8": {},
        "us.anthropic.claude-opus-4-5-20251101-v1:0": {},
        "us.anthropic.claude-sonnet-4-5-20250929-v1:0": {},
        "us.anthropic.claude-haiku-4-5-20251001-v1:0": {}
      }

Doctor

Paste the relevant parts of /doctor command from chat if applicable:

ECA version: 0.140.0

... leaving out these sections due to paths and such I don't want to leak ...

Logged providers: None
Relevant env vars: 


Credential files: None found

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Add any other context about the problem here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions