Skip to content

fix(anthropic): use adaptive thinking for Claude 4.6+ served outside the canonical registry - #9840

Closed
hammadxcm wants to merge 2 commits into
aaif-goose:mainfrom
hammadxcm:fix/adaptive-thinking-opus-4-7-4-8
Closed

fix(anthropic): use adaptive thinking for Claude 4.6+ served outside the canonical registry#9840
hammadxcm wants to merge 2 commits into
aaif-goose:mainfrom
hammadxcm:fix/adaptive-thinking-opus-4-7-4-8

Conversation

@hammadxcm

Copy link
Copy Markdown
Contributor

Summary

Fixes #9746

Claude Opus 4.7 and 4.8 require adaptive thinking and reject the deprecated manual shape with a 400:

"thinking.type.enabled" is not supported for this model.
Use "thinking.type.adaptive" and "output_config.effort" to control thinking behavior.

thinking_type_for_provider decides between adaptive and enabled from the canonical model registry's thinking_mode. That field is only populated for the first-party anthropic/ model IDs (anthropic/claude-opus-4.7, anthropic/claude-opus-4.8, …). The same models served through another provider — Bedrock, Vertex, Azure, or a fully custom Anthropic-compatible endpoint — have no registry thinking_mode, so they fall through to {"type": "enabled"} and 400. That's exactly why the reporter saw 4.6 work but 4.7/4.8 fail via Azure AI Foundry.

A pure registry/data fix wouldn't help a custom provider whose model isn't in the registry at all, so the fix belongs at the decision point.

Changes

crates/goose/src/providers/formats/anthropic.rs:

  • Add inferred_adaptive_thinking_mode(model_name), used only as a fallback when the registry returns no thinking_mode. Claude Opus/Sonnet 4.6 and later infer Adaptive; the Fable family infers AlwaysOnAdaptive; pre-4.6 and date-suffixed legacy names (claude-3-opus-20240229, Sonnet 4.5) stay on the enabled path.
  • The version parse is separator-agnostic (opus-4-7, opus-4.7, opus-4.8-fast) and forward-compatible, so future releases (4.9, 5.x) are covered without another change — addressing the issue's "should apply to any future Claude".
  • The registry still wins whenever it has a value; this only fills the gap.

Testing

  • test_inferred_adaptive_thinking_mode — unit coverage for the name parser across separators, suffixes, the Fable family, and the pre-4.6 / legacy-date negatives.
  • test_thinking_type_adaptive_for_non_registry_provider — Opus 4.7/4.8 through a provider with no registry entry now resolve to Adaptive, while Opus 4.5 still resolves to Enabled.
  • cargo fmt, cargo test -p goose --lib providers::formats::anthropic (38/38), cargo clippy -p goose --lib --tests --features aws-providers -- -D warnings all clean.

Note

The build-time registry inference (inferred_thinking_mode in build_canonical_models.rs) has the same first-party-only matching. I kept this PR to the runtime path since that's what fixes custom/compatible providers and doesn't require regenerating the whole canonical_models.json; aligning the build-time inference could be a small follow-up.

…egistry

Claude Opus 4.7 and 4.8 (and Sonnet 4.6, Fable) require adaptive thinking
and reject the deprecated `thinking: {type: "enabled"}` shape with a 400:

  "thinking.type.enabled" is not supported for this model. Use
  "thinking.type.adaptive" and "output_config.effort" ...

The canonical model registry only carries `thinking_mode` for the
first-party `anthropic/` model IDs, so the same models served through
other providers (Bedrock, Vertex, Azure) or through fully custom
Anthropic-compatible providers fall through to the enabled shape and 400.
This is why 4.6 worked but 4.7/4.8 failed via Azure AI Foundry.

Add a model-name fallback used only when the registry has no thinking_mode:
Claude Opus/Sonnet 4.6 and later infer adaptive, the Fable family infers
always-on adaptive, and pre-4.6 / date-suffixed legacy names are left on
the enabled path. The version parse is forward-compatible, so future
Claude releases (4.9, 5.x) are covered without another change.

Fixes aaif-goose#9746

Signed-off-by: hammadxcm <hammadkhanxcm@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83fd44b486

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

let after = name.split_once(family)?.1.trim_start_matches(['-', '.']);
let mut parts = after.split(['-', '.']);
let major = parts.next()?.parse().ok()?;
let minor = parts.next().and_then(|p| p.parse().ok()).unwrap_or(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep date-suffixed Claude 4 on enabled thinking

When the model is an official date-suffixed Claude 4 ID such as claude-opus-4-20250514 or claude-sonnet-4-20250514, this parser treats the release date as the minor version ((4, 20250514)), so the new >= (4, 6) fallback classifies pre-4.6 models as adaptive. The canonical entries for claude-opus-4/claude-sonnet-4 have reasoning=true but no thinking_mode, so requests with thinking enabled now send thinking.type=adaptive instead of the previous enabled-budget shape, which can make these older Claude 4 models reject the request.

Useful? React with 👍 / 👎.

@kimnamu kimnamu left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for tackling this, @hammadxcm — framing the fix at the decision point (thinking_type_for_provider) rather than as a registry data patch is the right call. I'm not a maintainer, just another Bedrock/Anthropic-provider user who reviewed this carefully and ran the branch locally (cargo test -p goose --lib providers::formats::anthropic, 38/38 green). A few things I verified that you may find useful:

What I confirmed is solid

  • The separator-agnostic parse and the major <= 9 guard correctly keep the legacy claude-3-*-<date> names off adaptive — the date lands in major, so (20240229, 0) is filtered out. Verified: claude-3-opus-20240229 -> None.
  • Bedrock-style prefixes are handled transparently: split_once("opus"/"sonnet") discards everything before the family token, so us.anthropic.claude-sonnet-4-7-v1:0 parses the same as claude-sonnet-4-7. Nice.

One regression to close (same as Codex's P2) — date-suffixed Claude 4 IDs

The date guard only covers the case where the date follows a single-digit major (claude-3-...). For the official date-suffixed Claude 4 IDs, the date lands in minor, so it slips through >= (4, 6):

claude-opus-4-20250514            -> (4, 20250514) -> Adaptive   (this is Opus 4.0)
claude-sonnet-4-20250514          -> (4, 20250514) -> Adaptive
claude-opus-4-1-20250805          -> (4, 20250805) -> Adaptive   (Opus 4.1)
us.anthropic.claude-sonnet-4-20250514-v1:0 -> Adaptive

These are real, in-registry IDs — canonical_models.json carries claude-opus-4-20250514 / claude-sonnet-4-20250514 with reasoning=true, thinking_mode=None, and the repo's own Bedrock fixture uses us.anthropic.claude-sonnet-4-20250514-v1:0. Because their canonical thinking_mode is None, they fall into exactly this new fallback, so a pre-4.6 model that used to send enabled would now send adaptive — the mirror-image 400 of the one you're fixing.

A one-character bound on minor (symmetric with your existing major bound) fixes it while staying forward-compatible with a hypothetical 4.10+. I applied this locally and it's green across the board. To lock the boundary in, here are the negatives to add to test_inferred_adaptive_thinking_mode (these fail on the current branch and pass with the bound — I verified both directions):

        // Official date-suffixed Claude 4 IDs (pre-4.6): the date must not
        // be parsed as the minor version and classified as adaptive.
        "claude-opus-4-20250514",
        "claude-sonnet-4-20250514",
        "claude-opus-4-1-20250805",

Red (current branch):

assertion `left == right` failed: claude-opus-4-20250514 should not infer adaptive
  left: Some(Adaptive)
 right: None

Green (with minor < 100): test result: ok. 38 passed; 0 failed.

Minor doc nit (non-blocking): the claude_family_version doc comment only mentions the date-in-major case (claude-3-opus-20240229); worth a half-line noting the date-in-minor case is handled by the minor bound in the caller, so the next reader doesn't re-introduce it.

Really clean, well-scoped fix otherwise — with the minor bound I'd love to see it land.

(Reviewed with the help of an AI agent (Claude Code); I ran the branch and verified every claim above myself.)

};

claude_family_version(&name, family)
.filter(|&(major, minor)| major <= 9 && (major, minor) >= (4, 6))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date-suffixed Claude 4 IDs (claude-opus-4-20250514, claude-opus-4-1-20250805, us.anthropic.claude-sonnet-4-20250514-v1:0) parse the date as minor and slip past >= (4, 6). These are real in-registry IDs with thinking_mode=None, so they hit this fallback and would flip a pre-4.6 model to adaptive (the mirror 400). A minor bound (symmetric with your major <= 9) fixes it and still allows a future 4.10+. Verified locally: red without it, 38/38 green with it.

Suggested change
.filter(|&(major, minor)| major <= 9 && (major, minor) >= (4, 6))
.filter(|&(major, minor)| major <= 9 && minor < 100 && (major, minor) >= (4, 6))

Date-suffixed Claude 4 IDs such as claude-opus-4-20250514 parsed the
release date as the minor version (4, 20250514), which passed the
>= (4, 6) check and wrongly inferred adaptive thinking for these
pre-4.6 models. They are real in-registry IDs with no thinking_mode,
so they reach this fallback.

Bound the minor version (minor < 100, symmetric with major <= 9) so
8-digit dates are rejected while future 4.10+ releases still infer
adaptive. Add the date-suffixed IDs as regression cases.

Thanks to @kimnamu and the codex review for catching this.

Signed-off-by: hammadxcm <hammadkhanxcm@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1ff0541d1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +143 to +144
let mode = canonical_thinking_mode(provider_name, &model_config.model_name)
.or_else(|| inferred_adaptive_thinking_mode(&model_config.model_name));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Infer reasoning with fallback thinking mode

When the only match is this new fallback (for example a custom-provider model like claude-opus-4.8-fast, which the parser test says should be covered, or a future claude-opus-4-9), canonical_reasoning still returns None because there is no canonical entry and ModelConfig::new(...).with_canonical_limits(custom_provider) leaves reasoning unset. As a result the subsequent reasoning != Some(true) check returns Disabled, so the request still omits thinking/output_config unless the caller explicitly set reasoning: true; the fallback needs to mark these inferred Claude modes as reasoning-capable too.

Useful? React with 👍 / 👎.

@DOsinga

DOsinga commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks for digging into this and for engaging so carefully with the review feedback — appreciated.

After looking at it closely, I think this fix is in the wrong layer, so I'm going to close it. The short version: the canonical model map (crates/goose-providers/src/canonical/data/canonical_models.json) already contains claude-opus-4.6/4.7/4.8 and claude-sonnet-4.6 with thinking_mode: adaptive (and claude-fable-5 as always_on_adaptive), and map_to_canonical_model already does fuzzy resolution — provider inference, Claude word-order swapping, prefix stripping and version-suffix normalization. So names like claude-opus-4-7, claude-opus-4.7 and even date-suffixed claude-opus-4-7-20260101 already resolve to the right canonical entry and pick up the adaptive thinking mode today, without any name-parsing heuristic.

Adding a parallel inference path in anthropic.rs means:

  • a second source of truth for thinking mode that will drift from the registry (the hardcoded >= 4.6 boundary has to be remembered and bumped by hand),
  • the date-vs-version ambiguity the review already surfaced (claude-opus-4-7 vs claude-opus-4-20250514), which the registry handles more robustly via suffix stripping, and
  • it only kicks in when reasoning is already configured, which is the inconsistency Codex flagged.

If you hit a model that genuinely does not resolve (e.g. the Bedrock-style us.anthropic.claude-opus-4-7-v1:0 / anthropic.claude-...-v1:0 forms currently return None), the right fix is in the canonical name normalization / registry data so that everything — thinking mode, pricing, context limits, temperature — gets resolved consistently from one place, rather than a thinking-specific heuristic.

Closing for now, but a PR that improves the canonical prefix/suffix normalization (or adds missing entries) for those provider-prefixed Bedrock names would be very welcome. Thanks again!

@DOsinga DOsinga closed this Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Opus 4.7/4.8 still use deprecated thinking.type.enabled after #7293

3 participants