Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

import logging
import os
from collections.abc import Sequence
from typing import Any, ClassVar, Generic, TypedDict
from typing import Any, ClassVar, Generic, TypedDict, cast

from agent_framework import (
ChatAndFunctionMiddlewareTypes,
Expand All @@ -18,6 +20,31 @@

from ._chat_client import AnthropicOptionsT, RawAnthropicClient

logger = logging.getLogger("agent_framework.anthropic")

DEFAULT_AWS_REGION = "us-east-1"


def _resolve_aws_region(aws_region: str | None, aws_profile: str | None) -> str:
if aws_region:
return aws_region

if env_region := os.getenv("AWS_REGION") or os.getenv("AWS_DEFAULT_REGION"):
return env_region

try:
import boto3

session: Any = boto3.Session(profile_name=aws_profile)
profile_region = cast(str | None, session.region_name)
if profile_region:
return profile_region
except ImportError:
pass

logger.warning("No AWS region specified, defaulting to %s", DEFAULT_AWS_REGION)
return DEFAULT_AWS_REGION


class AnthropicBedrockSettings(TypedDict, total=False):
"""Resolved settings for Anthropic Bedrock wrappers."""
Expand Down Expand Up @@ -90,7 +117,7 @@ def __init__(
anthropic_client = AsyncAnthropicBedrock(
aws_secret_key=secret_key_secret.get_secret_value() if secret_key_secret is not None else None,
aws_access_key=access_key_secret.get_secret_value() if access_key_secret is not None else None,
aws_region=settings.get("aws_region"),
aws_region=_resolve_aws_region(settings.get("aws_region"), settings.get("aws_profile")),
aws_profile=settings.get("aws_profile"),
aws_session_token=session_token_secret.get_secret_value() if session_token_secret is not None else None,
base_url=settings.get("anthropic_bedrock_base_url"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
logger = logging.getLogger("agent_framework.anthropic")

ANTHROPIC_DEFAULT_MAX_TOKENS: Final[int] = 1024
BETA_FLAGS: Final[list[str]] = ["mcp-client-2025-04-04", "code-execution-2025-08-25"]
BETA_FLAGS: Final[list[str]] = ["mcp-client-2025-04-04"]
UNSUPPORTED_SAMPLING_OPTIONS: Final[tuple[str, ...]] = ("temperature", "top_p", "top_k")

ResponseModelT = TypeVar("ResponseModelT", bound=BaseModel | None, default=None)
AnthropicAsyncClient = AsyncAnthropic | AsyncAnthropicBedrock | AsyncAnthropicFoundry | AsyncAnthropicVertex
Expand Down Expand Up @@ -150,8 +151,8 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
a default of 1024 will be used.

Keys:
temperature: Sampling temperature between 0 and 1.
top_p: Nucleus sampling parameter.
temperature: Unsupported by Anthropic SDK 1.x; ignored with a warning.
top_p: Unsupported by Anthropic SDK 1.x; ignored with a warning.
max_tokens: Maximum number of tokens to generate (REQUIRED).
stop: Stop sequences,
translates to ``stop_sequences`` in Anthropic API.
Expand All @@ -163,7 +164,7 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
instructions: System instructions for the model, translating to ``system`` in
the Anthropic API. Use a string for generic instructions or structured
Anthropic system blocks when you need prompt-cache ``cache_control``.
top_k: Number of top tokens to consider for sampling.
top_k: Unsupported by Anthropic SDK 1.x; ignored with a warning.
service_tier: Service tier ("auto" or "standard_only").
thinking: Extended thinking configuration for Claude models.
When enabled, responses include ``thinking`` content blocks showing Claude's
Expand All @@ -174,8 +175,7 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
additional_beta_flags: Additional beta flags to enable on the request.
"""

# Anthropic-specific generation parameters (supported by all models)
top_k: int
# Anthropic-specific generation parameters
service_tier: Literal["auto", "standard_only"]

# Extended thinking (Claude models)
Expand All @@ -191,6 +191,9 @@ class AnthropicChatOptions(ChatOptions[ResponseModelT], Generic[ResponseModelT],
additional_beta_flags: list[str]

# Unsupported base options (override with None to indicate not supported)
temperature: None # type: ignore[misc]
top_p: None # type: ignore[misc]
top_k: None
logit_bias: None # type: ignore[misc]
seed: None # type: ignore[misc]
frequency_penalty: None # type: ignore[misc]
Expand Down Expand Up @@ -229,6 +232,20 @@ def _apply_option_translations(options: dict[str, Any]) -> None:
options.setdefault(new_key, old_value)


def _drop_unsupported_sampling_options(options: dict[str, Any]) -> None:
unsupported_options = [name for name in UNSUPPORTED_SAMPLING_OPTIONS if name in options]
if not unsupported_options:
return

for name in unsupported_options:
options.pop(name)

logger.warning(
"Ignoring unsupported Anthropic sampling options: %s",
", ".join(unsupported_options),
)


# region Role and Finish Reason Maps


Expand Down Expand Up @@ -332,7 +349,7 @@ def __init__(
This can be used to further configure the client before passing it in.
For instance if you need to set a different base_url for testing or private deployments.
additional_beta_flags: Additional beta flags to enable on the client.
Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25".
The default flag is "mcp-client-2025-04-04".
additional_properties: Additional properties stored on the client instance.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.
Expand Down Expand Up @@ -714,6 +731,7 @@ def _prepare_options(
}
_apply_option_translations(filtered_kwargs)
run_options.update(filtered_kwargs)
_drop_unsupported_sampling_options(run_options)

# system message - Anthropic expects system instructions as a separate request parameter
instructions = options.get("instructions")
Expand Down Expand Up @@ -1774,7 +1792,7 @@ def __init__(
This can be used to further configure the client before passing it in.
For instance if you need to set a different base_url for testing or private deployments.
additional_beta_flags: Additional beta flags to enable on the client.
Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25".
The default flag is "mcp-client-2025-04-04".
additional_properties: Additional properties stored on the client instance.
middleware: Optional middleware to apply to the client.
function_invocation_configuration: Optional function invocation configuration override.
Expand Down
2 changes: 1 addition & 1 deletion python/packages/anthropic/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ classifiers = [
]
dependencies = [
"agent-framework-core>=1.19.0,<2",
"anthropic>=0.80.0,<0.117.0",
"anthropic>=1.11.0,<2",
Comment thread
eavanvalkenburg marked this conversation as resolved.
Comment thread
eavanvalkenburg marked this conversation as resolved.
Comment thread
eavanvalkenburg marked this conversation as resolved.
Comment thread
eavanvalkenburg marked this conversation as resolved.
]

[tool.uv]
Expand Down
36 changes: 26 additions & 10 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import os
import re
from pathlib import Path
from typing import Annotated, Any, cast
from unittest.mock import MagicMock, patch

import anthropic as anthropic_sdk
import httpx
import httpx2
import pytest
from agent_framework import (
Agent,
Expand Down Expand Up @@ -1129,13 +1130,12 @@ async def test_prepare_options_basic(mock_anthropic_client: MagicMock) -> None:
client = create_test_anthropic_client(mock_anthropic_client)

messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(max_tokens=100, temperature=0.7)
chat_options = ChatOptions(max_tokens=100)

run_options = client._prepare_options(messages, chat_options)

assert run_options["model"] == client.model
assert run_options["max_tokens"] == 100
assert run_options["temperature"] == 0.7
assert "messages" in run_options


Expand Down Expand Up @@ -1479,16 +1479,23 @@ async def test_prepare_options_with_stop_sequences(
assert run_options["stop_sequences"] == ["STOP", "END"]


async def test_prepare_options_with_top_p(mock_anthropic_client: MagicMock) -> None:
"""Test _prepare_options with top_p."""
def test_prepare_options_ignores_unsupported_sampling_options(
mock_anthropic_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Unsupported sampling options are dropped before calling Anthropic SDK 1.x."""
client = create_test_anthropic_client(mock_anthropic_client)

messages = [Message(role="user", contents=["Hello"])]
chat_options = ChatOptions(top_p=0.9)
chat_options = {"temperature": 0.7, "top_p": 0.9, "top_k": 40}

run_options = client._prepare_options(messages, chat_options)
with caplog.at_level(logging.WARNING, logger="agent_framework.anthropic"):
run_options = client._prepare_options(messages, chat_options)

assert run_options["top_p"] == 0.9
assert "temperature" not in run_options
assert "top_p" not in run_options
assert "top_k" not in run_options
assert "Ignoring unsupported Anthropic sampling options: temperature, top_p, top_k" in caplog.text


async def test_prepare_options_excludes_stream_option(
Expand All @@ -1505,6 +1512,15 @@ async def test_prepare_options_excludes_stream_option(
assert "stream" not in run_options


def test_prepare_betas_uses_only_required_default_flags(
mock_anthropic_client: MagicMock,
) -> None:
"""GA features must not add redundant beta headers."""
client = create_test_anthropic_client(mock_anthropic_client)

assert client._prepare_betas({}) == {"mcp-client-2025-04-04"}


async def test_prepare_options_consumes_additional_beta_flags(
mock_anthropic_client: MagicMock,
) -> None:
Expand Down Expand Up @@ -2001,8 +2017,8 @@ async def mock_stream():
def _anthropic_status_error(
error_cls: type[anthropic_sdk.APIStatusError], status_code: int, message: str
) -> anthropic_sdk.APIStatusError:
request = httpx.Request("POST", "https://api.anthropic.com/v1/messages")
response = httpx.Response(status_code, request=request, json={"error": {"message": message}})
request = httpx2.Request("POST", "https://api.anthropic.com/v1/messages")
response = httpx2.Response(status_code, request=request, json={"error": {"message": message}})
return error_cls(message, response=response, body={"error": {"message": message}})


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft. All rights reserved.

import logging
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
Expand Down Expand Up @@ -211,6 +212,50 @@ def test_raw_anthropic_bedrock_client_creates_sdk_client_from_arguments(
assert type(factory.call_args.kwargs[key]) is str


def test_raw_anthropic_bedrock_client_preserves_default_region(
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
monkeypatch.delenv("AWS_REGION", raising=False)
monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False)
mock_transport = _create_mock_transport("https://bedrock-runtime.us-east-1.amazonaws.com")

with (
patch("boto3.Session") as session_factory,
patch(
"agent_framework_anthropic._bedrock_client.AsyncAnthropicBedrock",
return_value=mock_transport,
) as factory,
caplog.at_level(logging.WARNING, logger="agent_framework.anthropic"),
):
session_factory.return_value.region_name = None
RawAnthropicBedrockClient(model="claude-bedrock-test")

assert factory.call_args.kwargs["aws_region"] == "us-east-1"
assert "No AWS region specified, defaulting to us-east-1" in caplog.text


def test_raw_anthropic_bedrock_client_uses_profile_region(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.delenv("AWS_REGION", raising=False)
monkeypatch.delenv("AWS_DEFAULT_REGION", raising=False)
mock_transport = _create_mock_transport("https://bedrock-runtime.eu-west-1.amazonaws.com")

with (
patch("boto3.Session") as session_factory,
patch(
"agent_framework_anthropic._bedrock_client.AsyncAnthropicBedrock",
return_value=mock_transport,
) as factory,
):
session_factory.return_value.region_name = "eu-west-1"
RawAnthropicBedrockClient(model="claude-bedrock-test", aws_profile="test-profile")

session_factory.assert_called_once_with(profile_name="test-profile")
assert factory.call_args.kwargs["aws_region"] == "eu-west-1"


@pytest.mark.parametrize("access_token", ["access-token", SecretString("access-token")], ids=["str", "secret"])
def test_raw_anthropic_vertex_client_creates_sdk_client_from_arguments(
access_token: str | SecretString,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import asyncio
import logging
from pathlib import Path
from typing import cast

from agent_framework import Agent, Content
from agent_framework.anthropic import AnthropicChatOptions, AnthropicClient
from anthropic import AsyncAnthropic
from dotenv import load_dotenv

# Load environment variables from .env file
Expand All @@ -17,8 +19,6 @@

This sample demonstrates using Anthropic with:
- Listing and using Anthropic-managed Skills.
- One approach to add additional beta flags.
You can also set additonal_chat_options with "additional_beta_flags" per request.
- Creating an agent with the Code Interpreter tool and a Skill.
- Catching and downloading generated files from the agent.

Expand All @@ -30,12 +30,13 @@

async def main() -> None:
"""Example of streaming response (get results as they are generated)."""
client = AnthropicClient[AnthropicChatOptions](additional_beta_flags=["skills-2025-10-02"])
client = AnthropicClient[AnthropicChatOptions]()
anthropic_client = cast(AsyncAnthropic, client.anthropic_client)

# List Anthropic-managed Skills
skills = await client.anthropic_client.beta.skills.list(source="anthropic", betas=["skills-2025-10-02"]) # type: ignore
skills = await anthropic_client.skills.list(source="anthropic")
for skill in skills.data:
print(f"{skill.source}: {skill.id} (version: {skill.latest_version})")
print(f"{skill.source}: {skill.id} (version: {skill.latest_version_id})")

# Create a agent with the pptx skill enabled
# Skills also need the code interpreter tool to function
Expand Down Expand Up @@ -87,9 +88,7 @@ async def main() -> None:
for idx, file in enumerate(files):
if file.file_id is None:
continue
file_content = await client.anthropic_client.beta.files.download( # type: ignore
file_id=file.file_id, betas=["files-api-2025-04-14"]
)
file_content = await anthropic_client.files.download(file_id=file.file_id)
with open(Path(__file__).parent / f"python_programming-{idx}.pptx", "wb") as f:
await file_content.write_to_file(f.name)
print(f"File {idx}: python_programming-{idx}.pptx saved to disk.")
Expand Down
13 changes: 7 additions & 6 deletions python/samples/02-agents/typed_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ async def demo_anthropic_chat_client() -> None:
# Create Anthropic client
client = AnthropicClient(model="claude-sonnet-4-5-20250929")

# Standard options work great:
# Supported standard and Anthropic-specific options are type checked:
response = await client.get_response(
[Message("user", contents=["What is the capital of France?"])],
options={
"temperature": 1, # Must be 1 when thinking is enabled
"max_tokens": 2048,
# Anthropic-specific options:
"thinking": {"type": "enabled", "budget_tokens": 1024},
# "top_k": 40, # <-- Uncomment for Anthropic-specific option
# Sampling options removed from Anthropic SDK 1.x are caught by the type checker:
# "temperature": 1,
# "top_k": 40,
},
)

Expand All @@ -68,9 +68,10 @@ async def demo_anthropic_agent() -> None:
name="claude-assistant",
instructions="You are a helpful assistant powered by Claude. Be concise.",
default_options={
"temperature": 0.5,
"max_tokens": 200,
"top_k": 40, # Anthropic-specific option, uncomment to try
# Sampling options removed from Anthropic SDK 1.x are caught by the type checker:
# "temperature": 0.5,
# "top_k": 40,
},
)

Expand Down
Loading
Loading