Skip to content
Open
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 @@ -3,7 +3,8 @@
import json
import logging
import sys
from collections.abc import AsyncGenerator, Callable
from collections.abc import AsyncGenerator, Callable, Mapping
from copy import copy
from typing import TYPE_CHECKING, Any, ClassVar

if sys.version_info >= (3, 12):
Expand Down Expand Up @@ -54,6 +55,7 @@
trace_chat_completion,
trace_streaming_chat_completion,
)
from semantic_kernel.utils.telemetry.user_agent import APP_INFO, prepend_semantic_kernel_to_user_agent

if TYPE_CHECKING:
from semantic_kernel.connectors.ai.function_call_choice_configuration import FunctionCallChoiceConfiguration
Expand Down Expand Up @@ -83,6 +85,7 @@ def __init__(
service_id: str | None = None,
api_key: str | None = None,
async_client: AsyncAnthropic | None = None,
default_headers: Mapping[str, str] | None = None,
env_file_path: str | None = None,
env_file_encoding: str | None = None,
) -> None:
Expand All @@ -95,6 +98,8 @@ def __init__(
api_key: The optional API key to use. If provided will override,
the env vars or .env file value.
async_client: An existing client to use.
default_headers: The default headers mapping of string keys to
string values for HTTP requests. (Optional)
env_file_path: Use the environment settings file as a fallback
to environment variables.
env_file_encoding: The encoding of the environment settings file.
Expand All @@ -112,9 +117,16 @@ def __init__(
if not anthropic_settings.chat_model_id:
raise ServiceInitializationError("The Anthropic chat model ID is required.")

# Merge APP_INFO into the headers if it exists

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dict(copy(default_headers)) is redundant — dict(default_headers) already produces a new dict from any Mapping. The copy() call (and the import) can be removed.

Suggested change
# Merge APP_INFO into the headers if it exists
merged_headers = dict(default_headers) if default_headers else {}

merged_headers = dict(copy(default_headers)) if default_headers else {}
if APP_INFO:
merged_headers.update(APP_INFO)
merged_headers = prepend_semantic_kernel_to_user_agent(merged_headers)

if not async_client:
async_client = AsyncAnthropic(
api_key=anthropic_settings.api_key.get_secret_value(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merged_headers is computed (including APP_INFO injection) for every call, but is only consumed in the if not async_client branch. When a caller passes a pre-built async_client, default_headers is silently discarded. Either emit a logger.warning here when both are provided, or raise early.

Suggested change
api_key=anthropic_settings.api_key.get_secret_value(),
if async_client and default_headers:
logger.warning(
"The `default_headers` parameter is ignored when `async_client` is provided. "
"Set headers directly on the supplied client instead."
)
# Merge APP_INFO into the headers if it exists
merged_headers = dict(default_headers) if default_headers else {}
if APP_INFO:
merged_headers.update(APP_INFO)
merged_headers = prepend_semantic_kernel_to_user_agent(merged_headers)

default_headers=merged_headers,
)

super().__init__(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -547,3 +547,68 @@ def test_chat_completion_reset_settings(

assert settings.tools is None
assert settings.tool_choice is None


def test_default_headers_with_app_info(anthropic_unit_test_env) -> None:
app_info = {"semantic-kernel-version": "python/1.0.0"}
mock_client = MagicMock(spec=AsyncAnthropic)
with (
patch(
"semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion.APP_INFO",
app_info,
),
patch(
"semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion.AsyncAnthropic",
return_value=mock_client,
) as mock_async_anthropic,
):
AnthropicChatCompletion()

mock_async_anthropic.assert_called_once()
call_kwargs = mock_async_anthropic.call_args.kwargs
headers = call_kwargs["default_headers"]
assert "semantic-kernel-version" in headers
assert headers["semantic-kernel-version"] == "python/1.0.0"
assert "User-Agent" in headers


def test_default_headers_merged_with_custom_headers(anthropic_unit_test_env) -> None:
app_info = {"semantic-kernel-version": "python/1.0.0"}
custom_headers = {"X-Custom-Header": "custom-value"}
mock_client = MagicMock(spec=AsyncAnthropic)
with (
patch(
"semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion.APP_INFO",
app_info,
),
patch(
"semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion.AsyncAnthropic",
return_value=mock_client,
) as mock_async_anthropic,
):
AnthropicChatCompletion(default_headers=custom_headers)

call_kwargs = mock_async_anthropic.call_args.kwargs
headers = call_kwargs["default_headers"]
assert headers["X-Custom-Header"] == "custom-value"
assert headers["semantic-kernel-version"] == "python/1.0.0"
assert "User-Agent" in headers


def test_default_headers_without_app_info(anthropic_unit_test_env) -> None:
mock_client = MagicMock(spec=AsyncAnthropic)
with (
patch(
"semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion.APP_INFO",
None,
),
patch(
"semantic_kernel.connectors.ai.anthropic.services.anthropic_chat_completion.AsyncAnthropic",
return_value=mock_client,
) as mock_async_anthropic,
):
AnthropicChatCompletion()

call_kwargs = mock_async_anthropic.call_args.kwargs
headers = call_kwargs["default_headers"]
assert headers == {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only covers APP_INFO=None without custom headers. Consider adding sibling tests for: (1) default_headers={"X-Custom": "value"} with APP_INFO=None to verify custom headers are still forwarded, and (2) both async_client and default_headers provided to document the expected behavior (warning, error, or documented no-op).

Loading