Skip to content

Commit 7a1ae43

Browse files
fix(models): correct thinking config for gateway model IDs and new model generations
- thinking.py now normalizes model IDs to their claude- core, so Bedrock (anthropic.claude-..., us.anthropic....-v1:0), LiteLLM (anthropic/claude-...), and Vertex (claude-...@Date) IDs classify like bare IDs instead of falling into the budget_tokens path (400 on >=4.7 generation models). - Inverted classification: adaptive thinking is the default; only the frozen legacy budget_tokens families (2.x/3.x, Haiku 4.5, Sonnet 4/4.5, Opus 4/4.1/4.5) keep the integer budget, so unknown future Claude models work by default. - New accepts_sampling_params() / supports_disabled_thinking() helpers; AndroidAgent act defaults now use make_non_thinking_settings(), which drops temperature on models that reject sampling params (Opus 4.7+, Sonnet 5, Fable 5) and omits thinking "disabled" on always-on models (Fable 5 / Mythos 5). - EffortLevel gains "xhigh" (supported from the Opus 4.7 generation on). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent cb40764 commit 7a1ae43

4 files changed

Lines changed: 219 additions & 22 deletions

File tree

src/askui/android_agent.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from askui.models.models import Point
1313
from askui.models.shared.secrets import Secret
1414
from askui.models.shared.settings import ActSettings, MessageSettings
15+
from askui.models.shared.thinking import make_non_thinking_settings
1516
from askui.models.shared.tools import Tool
1617
from askui.models.shared.truncation_strategies import TruncationStrategy
1718
from askui.prompts.act_prompts import create_android_agent_prompt
@@ -116,12 +117,13 @@ def __init__(
116117
image_scaler=self._vlm_provider.image_scaler,
117118
)
118119
self.act_tool_collection.add_agent_os(self.act_agent_os_facade)
119-
# Override default act settings with Android-specific settings
120+
# Override default act settings with Android-specific settings:
121+
# thinking disabled, temperature 0 — where the model still accepts
122+
# those (newer generations reject one or both).
120123
self.act_settings = ActSettings(
121124
messages=MessageSettings(
122125
system=create_android_agent_prompt(),
123-
thinking={"type": "disabled"},
124-
temperature=0.0,
126+
**make_non_thinking_settings(self._vlm_provider.model_id),
125127
),
126128
)
127129

src/askui/models/shared/agent_message_param.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,9 @@ class BetaRedactedThinkingBlock(BaseModel):
136136
# adaptive thinking (`thinking={"type": "adaptive"}`). It replaces the integer
137137
# `budget_tokens` used with `thinking={"type": "enabled", ...}` on older models.
138138
# Anthropic maps this to `output_config.effort`; providers that do not support
139-
# it (e.g. OpenAI chat) ignore it.
140-
EffortLevel = Literal["low", "medium", "high", "max"]
139+
# it (e.g. OpenAI chat) ignore it. "xhigh" (between "high" and "max") is
140+
# supported from the Opus 4.7 generation onward; the 4.6 generation rejects it.
141+
EffortLevel = Literal["low", "medium", "high", "xhigh", "max"]
141142

142143

143144
class UsageParam(BaseModel):

src/askui/models/shared/thinking.py

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@
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
1927
enable thinking for a given ``model_id``, so agents can turn thinking on by
2028
default without knowing which generation they run on. Callers can still override
@@ -27,33 +35,115 @@
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+
4691
def 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

59149
def 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

tests/unit/models/test_thinking.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
import pytest
44

55
from askui.models.shared.thinking import (
6+
accepts_sampling_params,
7+
make_non_thinking_settings,
68
make_thinking_settings,
9+
supports_disabled_thinking,
710
uses_adaptive_thinking,
811
)
912

@@ -17,6 +20,15 @@
1720
"claude-opus-4-8",
1821
"claude-opus-5",
1922
"claude-fable-5",
23+
"claude-haiku-5", # unknown future model -> adaptive by default
24+
# Gateway-prefixed IDs (Bedrock, LiteLLM, Vertex) classify like bare IDs.
25+
"anthropic.claude-opus-4-8",
26+
"us.anthropic.claude-opus-4-8-v1:0",
27+
"eu.anthropic.claude-sonnet-5-v1:0",
28+
"anthropic/claude-opus-4-8",
29+
"anthropic/claude-fable-5",
30+
"bedrock/us.anthropic.claude-opus-4-7-v1:0",
31+
"vertex_ai/claude-sonnet-4-6",
2032
]
2133

2234
_BUDGET_MODELS = [
@@ -25,6 +37,11 @@
2537
"claude-opus-4-5-20251101",
2638
"claude-opus-4-1-20250805",
2739
"claude-haiku-4-5-20251001",
40+
"claude-3-5-sonnet-20241022",
41+
"anthropic.claude-sonnet-4-5-20250929-v1:0",
42+
"us.anthropic.claude-3-5-sonnet-20241022-v2:0",
43+
"anthropic/claude-haiku-4-5",
44+
"claude-opus-4-5@20251101", # Vertex version separator
2845
"gpt-5.4",
2946
"some-unknown-model",
3047
]
@@ -62,3 +79,66 @@ def test_sonnet_5_is_not_confused_with_sonnet_4_5() -> None:
6279
# "claude-sonnet-5" must not match the older "claude-sonnet-4-5" prefix.
6380
assert uses_adaptive_thinking("claude-sonnet-5") is True
6481
assert uses_adaptive_thinking("claude-sonnet-4-5") is False
82+
83+
84+
@pytest.mark.parametrize(
85+
("model_id", "expected"),
86+
[
87+
("claude-sonnet-4-5-20250929", True), # legacy: sampling params fine
88+
("claude-haiku-4-5", True),
89+
("claude-sonnet-4-6", True), # 4.6 generation still accepts them
90+
("claude-opus-4-6", True),
91+
("claude-opus-4-7", False), # removed from Opus 4.7 onward
92+
("claude-opus-4-8", False),
93+
("claude-sonnet-5", False),
94+
("claude-fable-5", False),
95+
("anthropic.claude-opus-4-8", False), # gateway IDs classified too
96+
("anthropic/claude-sonnet-5", False),
97+
("gpt-5.4", True), # non-Claude: other providers manage their own
98+
],
99+
)
100+
def test_accepts_sampling_params(model_id: str, expected: bool) -> None:
101+
assert accepts_sampling_params(model_id) is expected
102+
103+
104+
@pytest.mark.parametrize(
105+
("model_id", "expected"),
106+
[
107+
("claude-sonnet-4-6", True),
108+
("claude-opus-4-8", True),
109+
("claude-sonnet-5", True),
110+
("claude-fable-5", False), # always-on thinking rejects "disabled"
111+
("claude-mythos-5", False),
112+
("anthropic/claude-fable-5", False),
113+
("gpt-5.4", True),
114+
],
115+
)
116+
def test_supports_disabled_thinking(model_id: str, expected: bool) -> None:
117+
assert supports_disabled_thinking(model_id) is expected
118+
119+
120+
def test_non_thinking_settings_keep_parity_on_older_models() -> None:
121+
assert make_non_thinking_settings("claude-sonnet-4-6") == {
122+
"thinking": {"type": "disabled"},
123+
"temperature": 0.0,
124+
}
125+
126+
127+
def test_non_thinking_settings_drop_temperature_from_opus_4_7_on() -> None:
128+
assert make_non_thinking_settings("claude-opus-4-8") == {
129+
"thinking": {"type": "disabled"},
130+
}
131+
assert make_non_thinking_settings("anthropic/claude-sonnet-5") == {
132+
"thinking": {"type": "disabled"},
133+
}
134+
135+
136+
def test_non_thinking_settings_omit_thinking_on_always_on_models() -> None:
137+
assert make_non_thinking_settings("claude-fable-5") == {}
138+
139+
140+
def test_effort_supports_xhigh() -> None:
141+
assert make_thinking_settings("claude-opus-4-8", effort="xhigh") == {
142+
"thinking": {"type": "adaptive"},
143+
"provider_options": {"output_config": {"effort": "xhigh"}},
144+
}

0 commit comments

Comments
 (0)