Skip to content
2 changes: 1 addition & 1 deletion python/packages/ag-ui/agent_framework_ag_ui/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ def _map_update(update: ChatResponseUpdate) -> ChatResponseUpdate:

@_apply_server_function_call_unwrap
class AGUIChatClient(
ChatMiddlewareLayer[AGUIChatOptionsT],
FunctionInvocationLayer[AGUIChatOptionsT],
ChatMiddlewareLayer[AGUIChatOptionsT],
ChatTelemetryLayer[AGUIChatOptionsT],
BaseChatClient[AGUIChatOptionsT],
Generic[AGUIChatOptionsT],
Expand Down
4 changes: 2 additions & 2 deletions python/packages/ag-ui/tests/ag_ui/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,16 @@ def pytest_configure() -> None:


class StreamingChatClientStub(
ChatMiddlewareLayer[OptionsCoT],
FunctionInvocationLayer[OptionsCoT],
ChatMiddlewareLayer[OptionsCoT],
ChatTelemetryLayer[OptionsCoT],
BaseChatClient[OptionsCoT],
Generic[OptionsCoT],
):
"""Typed streaming stub that satisfies SupportsChatGetResponse."""

def __init__(self, stream_fn: StreamFn, response_fn: ResponseFn | None = None) -> None:
super().__init__(function_middleware=[])
super().__init__(middleware=[])
self._stream_fn = stream_fn
self._response_fn = response_fn
self.last_session: AgentSession | None = None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import importlib.metadata

from ._chat_client import AnthropicChatOptions, AnthropicClient
from ._chat_client import AnthropicChatOptions, AnthropicClient, RawAnthropicClient

try:
__version__ = importlib.metadata.version(__name__)
Expand All @@ -12,5 +12,6 @@
__all__ = [
"AnthropicChatOptions",
"AnthropicClient",
"RawAnthropicClient",
"__version__",
]
131 changes: 114 additions & 17 deletions python/packages/anthropic/agent_framework_anthropic/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
__all__ = [
"AnthropicChatOptions",
"AnthropicClient",
"RawAnthropicClient",
"ThinkingConfig",
]

Expand Down Expand Up @@ -210,14 +211,24 @@ class AnthropicSettings(TypedDict, total=False):
chat_model_id: str | None


class AnthropicClient(
ChatMiddlewareLayer[AnthropicOptionsT],
FunctionInvocationLayer[AnthropicOptionsT],
ChatTelemetryLayer[AnthropicOptionsT],
class RawAnthropicClient(
BaseChatClient[AnthropicOptionsT],
Generic[AnthropicOptionsT],
):
"""Anthropic Chat client with middleware, telemetry, and function invocation support."""
"""Raw Anthropic chat client without middleware, telemetry, or function invocation support.

Warning:
**This class should not normally be used directly.** It does not include middleware,
telemetry, or function invocation support that you most likely need. If you do use it,
you should consider which additional layers to apply. There is a defined ordering that
you should follow:

1. **FunctionInvocationLayer** - Owns the tool/function calling loop and routes function middleware
2. **ChatMiddlewareLayer** - Applies chat middleware per model call and stays outside telemetry
3. **ChatTelemetryLayer** - Must stay inside chat middleware for correct per-call telemetry

Use ``AnthropicClient`` instead for a fully-featured client with all layers applied.
"""

OTEL_PROVIDER_NAME: ClassVar[str] = "anthropic" # type: ignore[reportIncompatibleVariableOverride, misc]

Expand All @@ -229,12 +240,10 @@ def __init__(
anthropic_client: AsyncAnthropic | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an Anthropic Agent client.
"""Initialize a raw Anthropic client.

Keyword Args:
api_key: The Anthropic API key to use for authentication.
Expand All @@ -245,37 +254,35 @@ def __init__(
additional_beta_flags: Additional beta flags to enable on the client.
Default flags are: "mcp-client-2025-04-04", "code-execution-2025-08-25".
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.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.

Examples:
.. code-block:: python

from agent_framework.anthropic import AnthropicClient
from agent_framework.anthropic import RawAnthropicClient
from azure.identity.aio import DefaultAzureCredential

# Using environment variables
# Set ANTHROPIC_API_KEY=your_anthropic_api_key
# ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929

# Or passing parameters directly
client = AnthropicClient(
client = RawAnthropicClient(
model_id="claude-sonnet-4-5-20250929",
api_key="your_anthropic_api_key",
)

# Or loading from a .env file
client = AnthropicClient(env_file_path="path/to/.env")
client = RawAnthropicClient(env_file_path="path/to/.env")

# Or passing in an existing client
from anthropic import AsyncAnthropic

anthropic_client = AsyncAnthropic(
api_key="your_anthropic_api_key", base_url="https://custom-anthropic-endpoint.com"
)
client = AnthropicClient(
client = RawAnthropicClient(
model_id="claude-sonnet-4-5-20250929",
anthropic_client=anthropic_client,
)
Expand All @@ -289,7 +296,7 @@ class MyOptions(AnthropicChatOptions, total=False):
my_custom_option: str


client: AnthropicClient[MyOptions] = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
client: RawAnthropicClient[MyOptions] = RawAnthropicClient(model_id="claude-sonnet-4-5-20250929")
response = await client.get_response("Hello", options={"my_custom_option": "value"})

"""
Expand Down Expand Up @@ -320,8 +327,6 @@ class MyOptions(AnthropicChatOptions, total=False):
# Initialize parent
super().__init__(
additional_properties=additional_properties,
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
)

# Initialize instance variables
Expand Down Expand Up @@ -1376,3 +1381,95 @@ def service_url(self) -> str:
The service URL for the chat client, or None if not set.
"""
return str(self.anthropic_client.base_url)


class AnthropicClient(
FunctionInvocationLayer[AnthropicOptionsT],
ChatMiddlewareLayer[AnthropicOptionsT],
ChatTelemetryLayer[AnthropicOptionsT],
RawAnthropicClient[AnthropicOptionsT],
Generic[AnthropicOptionsT],
):
"""Anthropic chat client with middleware, telemetry, and function invocation support."""

def __init__(
self,
*,
api_key: str | None = None,
model_id: str | None = None,
anthropic_client: AsyncAnthropic | None = None,
additional_beta_flags: list[str] | None = None,
additional_properties: dict[str, Any] | None = None,
middleware: Sequence[ChatAndFunctionMiddlewareTypes] | None = None,
function_invocation_configuration: FunctionInvocationConfiguration | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
"""Initialize an Anthropic client.

Keyword Args:
api_key: The Anthropic API key to use for authentication.
model_id: The ID of the model to use.
anthropic_client: An existing Anthropic client to use. If not provided, one will be created.
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".
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.
env_file_path: Path to environment file for loading settings.
env_file_encoding: Encoding of the environment file.

Examples:
.. code-block:: python

from agent_framework.anthropic import AnthropicClient

# Using environment variables
# Set ANTHROPIC_API_KEY=your_anthropic_api_key
# ANTHROPIC_CHAT_MODEL_ID=claude-sonnet-4-5-20250929

# Or passing parameters directly
client = AnthropicClient(
model_id="claude-sonnet-4-5-20250929",
api_key="your_anthropic_api_key",
)

# Or loading from a .env file
client = AnthropicClient(env_file_path="path/to/.env")

# Or passing in an existing client
from anthropic import AsyncAnthropic

anthropic_client = AsyncAnthropic(
api_key="your_anthropic_api_key", base_url="https://custom-anthropic-endpoint.com"
)
client = AnthropicClient(
model_id="claude-sonnet-4-5-20250929",
anthropic_client=anthropic_client,
)

# Using custom ChatOptions with type safety:
from typing import TypedDict
from agent_framework.anthropic import AnthropicChatOptions


class MyOptions(AnthropicChatOptions, total=False):
my_custom_option: str


client: AnthropicClient[MyOptions] = AnthropicClient(model_id="claude-sonnet-4-5-20250929")
response = await client.get_response("Hello", options={"my_custom_option": "value"})
"""
super().__init__(
api_key=api_key,
model_id=model_id,
anthropic_client=anthropic_client,
additional_beta_flags=additional_beta_flags,
additional_properties=additional_properties,
middleware=middleware,
function_invocation_configuration=function_invocation_configuration,
env_file_path=env_file_path,
env_file_encoding=env_file_encoding,
)
20 changes: 19 additions & 1 deletion python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@

import pytest
from agent_framework import (
ChatMiddlewareLayer,
ChatOptions,
ChatResponseUpdate,
Content,
FunctionInvocationLayer,
Message,
SupportsChatGetResponse,
tool,
)
from agent_framework._settings import load_settings
from agent_framework._tools import SHELL_TOOL_KIND_VALUE
from agent_framework.observability import ChatTelemetryLayer
from anthropic.types.beta import (
BetaMessage,
BetaTextBlock,
Expand All @@ -23,7 +26,7 @@
)
from pydantic import BaseModel, Field

from agent_framework_anthropic import AnthropicClient
from agent_framework_anthropic import AnthropicClient, RawAnthropicClient
from agent_framework_anthropic._chat_client import AnthropicSettings

# Test constants
Expand Down Expand Up @@ -64,6 +67,8 @@ def create_test_anthropic_client(
client.additional_beta_flags = []
client.chat_middleware = []
client.function_middleware = []
client._cached_chat_middleware_pipeline = None
client._cached_function_middleware_pipeline = None
client.function_invocation_configuration = normalize_function_invocation_configuration(None)

return client
Expand Down Expand Up @@ -117,6 +122,19 @@ def test_anthropic_client_init_with_client(mock_anthropic_client: MagicMock) ->
assert isinstance(client, SupportsChatGetResponse)


def test_anthropic_client_wraps_raw_client_with_standard_layer_order() -> None:
"""Test AnthropicClient composes the standard public layer stack around the raw client."""
assert issubclass(AnthropicClient, RawAnthropicClient)
mro = AnthropicClient.__mro__
assert mro.index(FunctionInvocationLayer) < mro.index(ChatMiddlewareLayer)
assert mro.index(ChatMiddlewareLayer) < mro.index(ChatTelemetryLayer)
assert mro.index(ChatTelemetryLayer) < mro.index(RawAnthropicClient)
# RawAnthropicClient must not include the convenience layers
assert not issubclass(RawAnthropicClient, FunctionInvocationLayer)
assert not issubclass(RawAnthropicClient, ChatMiddlewareLayer)
assert not issubclass(RawAnthropicClient, ChatTelemetryLayer)


def test_anthropic_client_init_auto_create_client(
anthropic_unit_test_env: dict[str, str],
) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ class AzureAIAgentOptions(ChatOptions, total=False):


class AzureAIAgentClient(
ChatMiddlewareLayer[AzureAIAgentOptionsT],
FunctionInvocationLayer[AzureAIAgentOptionsT],
ChatMiddlewareLayer[AzureAIAgentOptionsT],
ChatTelemetryLayer[AzureAIAgentOptionsT],
BaseChatClient[AzureAIAgentOptionsT],
Generic[AzureAIAgentOptionsT],
Expand Down
8 changes: 4 additions & 4 deletions python/packages/azure-ai/agent_framework_azure_ai/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ class RawAzureAIClient(RawOpenAIResponsesClient[AzureAIClientOptionsT], Generic[
you should consider which additional layers to apply. There is a defined ordering that
you should follow:

1. **ChatMiddlewareLayer** - Should be applied first as it also prepares function middleware
2. **FunctionInvocationLayer** - Handles tool/function calling loop
3. **ChatTelemetryLayer** - Must be inside the function calling loop for correct per-call telemetry
1. **FunctionInvocationLayer** - Owns the tool/function calling loop and routes function middleware
2. **ChatMiddlewareLayer** - Applies chat middleware per model call and stays outside telemetry
3. **ChatTelemetryLayer** - Must stay inside chat middleware for correct per-call telemetry

Use ``AzureAIClient`` instead for a fully-featured client with all layers applied.
"""
Expand Down Expand Up @@ -1214,8 +1214,8 @@ def as_agent(


class AzureAIClient(
ChatMiddlewareLayer[AzureAIClientOptionsT],
FunctionInvocationLayer[AzureAIClientOptionsT],
ChatMiddlewareLayer[AzureAIClientOptionsT],
ChatTelemetryLayer[AzureAIClientOptionsT],
RawAzureAIClient[AzureAIClientOptionsT],
Generic[AzureAIClientOptionsT],
Expand Down
6 changes: 6 additions & 0 deletions python/packages/azure-ai/tests/test_azure_ai_agent_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def create_test_azure_ai_chat_client(
client.middleware = None
client.chat_middleware = []
client.function_middleware = []
client._cached_chat_middleware_pipeline = None
client._cached_function_middleware_pipeline = None
client.otel_provider_name = "azure.ai"
client.function_invocation_configuration = {
"enabled": True,
Expand Down Expand Up @@ -151,6 +153,10 @@ def test_azure_ai_chat_client_init_auto_create_client(
chat_client.agent_name = None
chat_client.additional_properties = {}
chat_client.middleware = None
chat_client.chat_middleware = []
chat_client.function_middleware = []
chat_client._cached_chat_middleware_pipeline = None
chat_client._cached_function_middleware_pipeline = None

assert chat_client.agents_client is mock_agents_client
assert chat_client.agent_id is None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,8 @@ class BedrockSettings(TypedDict, total=False):


class BedrockChatClient(
ChatMiddlewareLayer[BedrockChatOptionsT],
FunctionInvocationLayer[BedrockChatOptionsT],
ChatMiddlewareLayer[BedrockChatOptionsT],
ChatTelemetryLayer[BedrockChatOptionsT],
BaseChatClient[BedrockChatOptionsT],
Generic[BedrockChatOptionsT],
Expand Down
11 changes: 1 addition & 10 deletions python/packages/core/agent_framework/_clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -966,16 +966,7 @@ def _apply_get_response_docstrings() -> None:
from .observability import ChatTelemetryLayer

apply_layered_docstring(ChatTelemetryLayer.get_response, BaseChatClient.get_response)
apply_layered_docstring(
FunctionInvocationLayer.get_response,
ChatTelemetryLayer.get_response,
extra_keyword_args={
"function_middleware": """
Optional per-call function middleware.
When omitted, middleware configured on the client or forwarded from higher layers is used.
""",
},
)
apply_layered_docstring(FunctionInvocationLayer.get_response, ChatTelemetryLayer.get_response)
apply_layered_docstring(
ChatMiddlewareLayer.get_response,
FunctionInvocationLayer.get_response,
Expand Down
Loading
Loading