1515 rejected, and ``effort`` is a separate parameter sent via
1616 ``output_config.effort`` (not part of ``thinking``).
1717
18+ The budget-token generation is a closed set, so classification is inverted:
19+ every Claude model *not* in the frozen legacy list is treated as adaptive,
20+ which makes unknown future models work by default. Model IDs are matched on
21+ their ``claude-...`` core, so gateway-prefixed identifiers (Bedrock
22+ ``anthropic.claude-opus-4-8`` or ``us.anthropic.claude-...-v1:0``, LiteLLM
23+ ``anthropic/claude-...``, Vertex ``claude-...@20260401``) resolve like the
24+ bare model ID.
25+
1826`make_thinking_settings()` returns the `MessageSettings` keyword arguments that
1927enable thinking for a given ``model_id``, so agents can turn thinking on by
2028default without knowing which generation they run on. Callers can still override
2735
2836_DEFAULT_BUDGET_TOKENS = 2048
2937
30- # Model-ID prefixes for Anthropic models that use adaptive thinking. These are
31- # the models where the integer `budget_tokens` is removed or deprecated in favour
32- # of adaptive thinking (`{"type": "adaptive"}`) plus the `effort` setting.
33- # `str.startswith` matches dated snapshots too (e.g. "claude-sonnet-4-6-20260401").
34- # Note: "claude-sonnet-5" does not match the older "claude-sonnet-4-5".
35- _ADAPTIVE_THINKING_MODEL_PREFIXES = (
38+ # Model-ID prefixes (after normalization) of the Anthropic model families that
39+ # take the fixed integer `budget_tokens`. This set is FROZEN: budget thinking
40+ # was replaced by adaptive thinking with the 4.6 generation, so no future model
41+ # will ever be added here.
42+ _LEGACY_BUDGET_THINKING_MODEL_PREFIXES = (
43+ "claude-2" ,
44+ "claude-instant" ,
45+ "claude-3-" ,
46+ "claude-haiku-4-5" ,
47+ "claude-sonnet-4-0" ,
48+ "claude-sonnet-4-1" ,
49+ "claude-sonnet-4-2" , # dated snapshots, e.g. "claude-sonnet-4-20250514"
50+ "claude-sonnet-4-5" ,
51+ "claude-opus-4-0" ,
52+ "claude-opus-4-1" ,
53+ "claude-opus-4-2" , # dated snapshots, e.g. "claude-opus-4-20250514"
54+ "claude-opus-4-5" ,
55+ )
56+
57+ # The one adaptive-thinking generation that still accepts sampling parameters
58+ # (temperature/top_p/top_k). From Opus 4.7 / Sonnet 5 / Fable 5 onward the API
59+ # rejects them with a 400.
60+ _SAMPLING_CAPABLE_ADAPTIVE_MODEL_PREFIXES = (
3661 "claude-sonnet-4-6" ,
37- "claude-sonnet-5" ,
3862 "claude-opus-4-6" ,
39- "claude-opus-4-7" ,
40- "claude-opus-4-8" ,
41- "claude-opus-5" ,
63+ )
64+
65+ # Models where thinking is always on: an explicit {"type": "disabled"} is
66+ # rejected with a 400, so the thinking field must be omitted entirely.
67+ _ALWAYS_ON_THINKING_MODEL_PREFIXES = (
4268 "claude-fable-5" ,
69+ "claude-mythos" ,
4370)
4471
4572
73+ def _normalize (model_id : str ) -> str | None :
74+ """Extract the ``claude-...`` core of a model ID.
75+
76+ Gateway wrappers then match like bare IDs (e.g.
77+ ``"us.anthropic.claude-opus-4-8-v1:0"`` and ``"anthropic/claude-opus-4-8"``
78+ both normalize to ``"claude-opus-4-8..."``).
79+
80+ Args:
81+ model_id (str): The (possibly gateway-prefixed) model identifier.
82+
83+ Returns:
84+ str | None: The model ID from its ``claude-`` core onward, or ``None``
85+ if the ID does not reference a Claude model.
86+ """
87+ index = model_id .find ("claude-" )
88+ return None if index < 0 else model_id [index :]
89+
90+
4691def uses_adaptive_thinking (model_id : str ) -> bool :
4792 """Whether ``model_id`` uses adaptive thinking instead of a token budget.
4893
94+ True for every Claude model outside the frozen legacy budget families (so
95+ unknown future models default to adaptive); False for non-Claude model IDs.
96+
4997 Args:
50- model_id (str): The Anthropic model identifier.
98+ model_id (str): The model identifier (bare or gateway-prefixed) .
5199
52100 Returns:
53101 bool: ``True`` if the model expects ``{"type": "adaptive"}`` and the
54102 `effort` setting, ``False`` if it expects a fixed ``budget_tokens``.
55103 """
56- return model_id .startswith (_ADAPTIVE_THINKING_MODEL_PREFIXES )
104+ normalized = _normalize (model_id )
105+ return normalized is not None and not normalized .startswith (
106+ _LEGACY_BUDGET_THINKING_MODEL_PREFIXES
107+ )
108+
109+
110+ def accepts_sampling_params (model_id : str ) -> bool :
111+ """Whether the model accepts sampling parameters such as ``temperature``.
112+
113+ False for adaptive-thinking Claude models newer than the 4.6 generation
114+ (Opus 4.7/4.8, Sonnet 5, Fable 5, and future models), which reject them
115+ with a 400. True for older Claude models and non-Claude model IDs (other
116+ providers manage their own sampling parameters).
117+
118+ Args:
119+ model_id (str): The model identifier (bare or gateway-prefixed).
120+
121+ Returns:
122+ bool: ``True`` if sampling parameters may be sent to the model.
123+ """
124+ normalized = _normalize (model_id )
125+ return normalized is None or normalized .startswith (
126+ _LEGACY_BUDGET_THINKING_MODEL_PREFIXES
127+ + _SAMPLING_CAPABLE_ADAPTIVE_MODEL_PREFIXES
128+ )
129+
130+
131+ def supports_disabled_thinking (model_id : str ) -> bool :
132+ """Whether the model accepts an explicit ``{"type": "disabled"}`` thinking config.
133+
134+ False for always-on-thinking models (Fable 5, Mythos 5), which reject it
135+ with a 400 — omit the thinking field there.
136+
137+ Args:
138+ model_id (str): The model identifier (bare or gateway-prefixed).
139+
140+ Returns:
141+ bool: ``True`` if ``{"type": "disabled"}`` may be sent to the model.
142+ """
143+ normalized = _normalize (model_id )
144+ return normalized is None or not normalized .startswith (
145+ _ALWAYS_ON_THINKING_MODEL_PREFIXES
146+ )
57147
58148
59149def make_thinking_settings (
@@ -75,11 +165,11 @@ def make_thinking_settings(
75165 ``thinking={"type": "enabled", "budget_tokens": 2048}`` and ignore ``effort``.
76166
77167 Args:
78- model_id (str): The Anthropic model identifier.
79- effort (EffortLevel | None, optional): How much the model should think and
80- act (``"low"``, ``"medium"``, ``"high"`` or ``"max "``). Only applied
81- for models that support adaptive thinking. Default: None (the model
82- uses its own default).
168+ model_id (str): The model identifier (bare or gateway-prefixed) .
169+ effort (EffortLevel | None, optional): How much the model should think
170+ and act (``"low"``, ``"medium"``, ``"high"``, ``"xhigh "`` or
171+ ``"max"``). Only applied for models that support adaptive thinking.
172+ Default: None (the model uses its own default).
83173
84174 Returns:
85175 dict[str, Any]: `MessageSettings` keyword arguments (``thinking`` and,
@@ -91,3 +181,27 @@ def make_thinking_settings(
91181 settings ["provider_options" ] = {"output_config" : {"effort" : effort }}
92182 return settings
93183 return {"thinking" : {"type" : "enabled" , "budget_tokens" : _DEFAULT_BUDGET_TOKENS }}
184+
185+
186+ def make_non_thinking_settings (model_id : str ) -> dict [str , Any ]:
187+ """Return `MessageSettings` keyword arguments for thinking-off, deterministic runs.
188+
189+ Used by device agents (Android) that historically pinned
190+ ``thinking={"type": "disabled"}`` and ``temperature=0.0``. Each field is
191+ included only where the model still accepts it: models from the Opus 4.7
192+ generation onward reject sampling parameters, and always-on-thinking models
193+ (Fable 5) reject an explicit ``"disabled"``.
194+
195+ Args:
196+ model_id (str): The model identifier (bare or gateway-prefixed).
197+
198+ Returns:
199+ dict[str, Any]: `MessageSettings` keyword arguments (``thinking``
200+ and/or ``temperature``, possibly empty).
201+ """
202+ settings : dict [str , Any ] = {}
203+ if supports_disabled_thinking (model_id ):
204+ settings ["thinking" ] = {"type" : "disabled" }
205+ if accepts_sampling_params (model_id ):
206+ settings ["temperature" ] = 0.0
207+ return settings
0 commit comments