Skip to content

Commit d08d75c

Browse files
its-amannvvlrff
andauthored
feat(anthropic): add typed speed option to AnthropicConfig (#3495)
* feat(anthropic): add typed inference speed configuration * feat(anthropic): introduce Speed type for inference speed configuration --------- Co-authored-by: Semen Frolov <148821259+vvlrff@users.noreply.github.com> Co-authored-by: Semen Frolov <vvlrff@gmail.com>
1 parent a116188 commit d08d75c

7 files changed

Lines changed: 109 additions & 9 deletions

File tree

‎ag2/config/anthropic/anthropic_client.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import logging
77
from collections.abc import Iterable, Sequence
88
from itertools import chain
9-
from typing import Any, TypedDict
9+
from typing import Any, Literal, TypedDict
1010

1111
import httpx2
1212
from anthropic import NOT_GIVEN, AsyncAnthropic
@@ -78,6 +78,9 @@ def _message_usage(message: Message) -> Usage:
7878
return normalize_usage(message.usage.model_dump() if message.usage else {})
7979

8080

81+
Speed = Literal["standard", "fast"]
82+
83+
8184
class CreateOptions(TypedDict, total=False):
8285
# `temperature`, `top_p` and `top_k` are absent on purpose: `anthropic` 1.x
8386
# removed them from the Messages API signature, so passing one here is a
@@ -89,6 +92,7 @@ class CreateOptions(TypedDict, total=False):
8992
stream: bool
9093
metadata: dict[str, str] | None
9194
service_tier: str | None
95+
speed: Speed | None
9296

9397

9498
class AnthropicClient(LLMClient):

‎ag2/config/anthropic/config.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from ag2.config.config import ModelConfig, ModelProvider
1313

14-
from .anthropic_client import AnthropicClient, CreateOptions
14+
from .anthropic_client import AnthropicClient, CreateOptions, Speed
1515
from .files import AnthropicFilesClient
1616
from .mappers import SAMPLING_FIELDS, merge_sampling_into_extra_body
1717

@@ -35,6 +35,7 @@ class AnthropicConfigOverrides(TypedDict, total=False):
3535
prompt_caching: bool
3636
cache_diagnostics: bool
3737
extra_body: dict[str, Any] | None
38+
speed: Speed | None
3839

3940

4041
@dataclass(slots=True)
@@ -57,6 +58,8 @@ class AnthropicConfig(ModelConfig):
5758
prompt_caching: bool = True
5859
cache_diagnostics: bool = False
5960
extra_body: dict[str, Any] | None = None
61+
speed: Speed | None = None
62+
"""Inference speed; some models require a fast-mode beta header for either value."""
6063

6164
@property
6265
def provider(self) -> ModelProvider:
@@ -78,6 +81,8 @@ def create(self) -> AnthropicClient:
7881
options["metadata"] = self.metadata
7982
if self.service_tier is not None:
8083
options["service_tier"] = self.service_tier
84+
if self.speed is not None:
85+
options["speed"] = self.speed
8186

8287
return AnthropicClient(
8388
api_key=self.api_key,

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ ag-ui = ["ag-ui-protocol>=1.0,<2"]
8888

8989
# === LLM providers (ag2.config) ===
9090
openai = ["openai>=3.27.0,<4"]
91-
anthropic = ["anthropic>=1.9.0,<2"]
91+
anthropic = ["anthropic>=1.15.0,<2"]
9292
gemini = [
9393
# Version bounds: local types in gemini_types.py are tested for subset compatibility
9494
# with the SDK (local can have extra fields for forward compatibility).

‎test/config/anthropic/test_anthropic_config.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
#
33
# SPDX-License-Identifier: Apache-2.0
44

5+
from typing import Literal
6+
7+
import pytest
8+
59
from ag2.config import AnthropicConfig
610
from ag2.config.anthropic import AnthropicClient
711

@@ -48,6 +52,22 @@ def test_max_tokens_can_be_overridden() -> None:
4852
assert config.max_tokens == 8192
4953

5054

55+
def test_speed_defaults_to_none() -> None:
56+
config = AnthropicConfig(model="claude-opus-5-5")
57+
assert config.speed is None
58+
59+
60+
@pytest.mark.parametrize("speed", ["standard", "fast", None])
61+
def test_copy_with_speed(speed: Literal["standard", "fast"] | None) -> None:
62+
config = AnthropicConfig(model="claude-opus-5-5", speed="fast")
63+
64+
copied = config.copy(speed=speed)
65+
66+
assert copied == AnthropicConfig(model="claude-opus-5-5", speed=speed)
67+
assert config.speed == "fast"
68+
assert copied is not config
69+
70+
5171
def test_extra_body_defaults_to_none() -> None:
5272
config = AnthropicConfig(model="claude-sonnet-5")
5373
assert config.extra_body is None

‎test/config/anthropic/test_request_body.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212

1313
import json
14+
from typing import Literal
1415

1516
import httpx2
1617
import pytest
@@ -123,6 +124,57 @@ async def test_sampling_fields_reach_the_api_when_streaming() -> None:
123124
assert captured["body"] == IsPartialDict({"temperature": 0.2, "stream": True})
124125

125126

127+
@pytest.mark.asyncio
128+
@pytest.mark.parametrize("streaming", [False, True])
129+
@pytest.mark.parametrize("speed", ["standard", "fast", None])
130+
async def test_speed_reaches_the_api_when_set(speed: Literal["standard", "fast"] | None, streaming: bool) -> None:
131+
captured: dict[str, object] = {}
132+
config = AnthropicConfig(
133+
model="claude-opus-5-5",
134+
api_key="test",
135+
speed=speed,
136+
streaming=streaming,
137+
http_client=_capturing_client(captured, stream=streaming),
138+
)
139+
140+
await _ask(config)
141+
142+
body = captured["body"]
143+
assert isinstance(body, dict)
144+
if speed is None:
145+
assert "speed" not in body
146+
else:
147+
assert body == IsPartialDict({"speed": speed})
148+
headers = captured["headers"]
149+
assert isinstance(headers, httpx2.Headers)
150+
assert "anthropic-beta" not in headers
151+
152+
153+
@pytest.mark.asyncio
154+
@pytest.mark.parametrize("streaming", [False, True])
155+
@pytest.mark.parametrize("speed", ["standard", "fast"])
156+
async def test_speed_preserves_the_users_beta_header_with_mcp(
157+
context: Context, speed: Literal["standard", "fast"], streaming: bool
158+
) -> None:
159+
captured: dict[str, object] = {}
160+
config = AnthropicConfig(
161+
model="claude-opus-5",
162+
api_key="test",
163+
speed=speed,
164+
streaming=streaming,
165+
default_headers={"Anthropic-Beta": "fast-mode-2026-02-01"},
166+
http_client=_capturing_client(captured, stream=streaming),
167+
)
168+
schemas = await MCPServerTool(server_url="https://mcp.example.com/x", server_label="x").schemas(context)
169+
170+
await _ask(config, tools=schemas)
171+
172+
assert captured["body"] == IsPartialDict({"speed": speed})
173+
headers = captured["headers"]
174+
assert isinstance(headers, httpx2.Headers)
175+
assert headers.get_list("anthropic-beta") == ["fast-mode-2026-02-01,mcp-client-2025-11-20"]
176+
177+
126178
@pytest.mark.asyncio
127179
async def test_user_extra_body_wins_over_a_sampling_field() -> None:
128180
captured: dict[str, object] = {}

‎uv.lock‎

Lines changed: 5 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎website/docs/user-guide/model_configuration.mdx‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ config = AnthropicConfig(
7777
)
7878
```
7979

80-
`ag2[anthropic]` requires `anthropic>=1.6.0,<2`. See [Anthropic Configuration](#anthropic-configuration) for the `httpx2` HTTP client and the sampling parameters (`temperature`, `top_p`, `top_k`).
80+
`ag2[anthropic]` requires `anthropic>=1.15.0,<2`. See [Anthropic Configuration](#anthropic-configuration) for the `httpx2` HTTP client and the sampling parameters (`temperature`, `top_p`, `top_k`).
8181
</Tab>
8282

8383
<Tab title="Gemini">
@@ -253,7 +253,7 @@ The model only returns its thinking when you set `summary`; without it the model
253253

254254
### Anthropic Configuration
255255

256-
`AnthropicConfig` connects to the [Claude API](https://platform.claude.com/docs/en/build-with-claude/overview){.external-link target="_blank"} through the official `anthropic` SDK. Install the optional dependency with `pip install "ag2[anthropic]"`, which requires `anthropic>=1.6.0,<2`.
256+
`AnthropicConfig` connects to the [Claude API](https://platform.claude.com/docs/en/build-with-claude/overview){.external-link target="_blank"} through the official `anthropic` SDK. Install the optional dependency with `pip install "ag2[anthropic]"`, which requires `anthropic>=1.15.0,<2`.
257257

258258
If `api_key` is not passed explicitly, it is resolved from the `ANTHROPIC_API_KEY` environment variable.
259259

@@ -264,6 +264,24 @@ from ag2.config import AnthropicConfig
264264
config = AnthropicConfig(model="claude-haiku-4-5-20251001", streaming=True)
265265
```
266266

267+
#### Inference Speed
268+
269+
Set `speed="fast"` or `speed="standard"` to select the inference speed on supported models. The default, `None`, omits the parameter and leaves the API's default behavior unchanged. The option works with both streaming and non-streaming requests, including `config.copy(speed="standard")` overrides.
270+
271+
Some models require the `fast-mode-2026-02-01` beta header for **either** explicit value, including `"standard"`. AG2 does not add this model-specific header automatically; set it through `default_headers` when required:
272+
273+
```python linenums="1"
274+
from ag2.config import AnthropicConfig
275+
276+
config = AnthropicConfig(
277+
model="claude-opus-5",
278+
speed="fast",
279+
default_headers={"anthropic-beta": "fast-mode-2026-02-01"},
280+
)
281+
```
282+
283+
Caller-supplied beta headers are preserved when AG2 adds its own tool-related betas. See [Anthropic's fast-mode guide](https://platform.claude.com/docs/en/build-with-claude/fast-mode){.external-link target="_blank"} for current model availability, header requirements and pricing. Fast mode has premium pricing; enabling it does not make an unsupported model compatible.
284+
267285
#### Custom HTTP Client
268286

269287
`anthropic>=1` is built on [`httpx2`](https://pypi.org/project/httpx2/){.external-link target="_blank"}, so `http_client` takes an `#!python httpx2.AsyncClient`:

0 commit comments

Comments
 (0)