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 @@ -31,8 +31,16 @@
from agent_framework._telemetry import get_user_agent, mark_feature_used
from agent_framework._tools import SHELL_TOOL_KIND_VALUE, normalize_tools
from agent_framework._types import _get_data_bytes_as_str # type: ignore
from agent_framework.exceptions import (
ChatClientException,
ChatClientInvalidAuthException,
ChatClientInvalidRequestException,
)
from agent_framework.observability import ChatTelemetryLayer
from anthropic import APIError as AnthropicAPIError
from anthropic import AsyncAnthropic, AsyncAnthropicFoundry
from anthropic import AuthenticationError as AnthropicAuthenticationError
from anthropic import BadRequestError as AnthropicBadRequestError
from anthropic.lib.bedrock import AsyncAnthropicBedrock
from anthropic.lib.vertex import AsyncAnthropicVertex
from anthropic.types.beta import (
Expand Down Expand Up @@ -553,17 +561,37 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]:
# accumulator to _process_stream_event to emit increments instead.
emitted_usage: dict[str, int] = {}
mark_feature_used(FeatureIndex.ANTHROPIC)
async for chunk in await self.anthropic_client.beta.messages.create(**run_options, stream=True):
parsed_chunk = self._process_stream_event(chunk, emitted_usage)
if parsed_chunk:
yield parsed_chunk
try:
async for chunk in await self.anthropic_client.beta.messages.create(**run_options, stream=True):
parsed_chunk = self._process_stream_event(chunk, emitted_usage)
if parsed_chunk:
yield parsed_chunk
except AnthropicAuthenticationError as ex:
raise ChatClientInvalidAuthException(
f"Anthropic authentication failed: {ex}", inner_exception=ex
) from ex
except AnthropicBadRequestError as ex:
raise ChatClientInvalidRequestException(
f"Invalid Anthropic request: {ex}", inner_exception=ex
) from ex
except AnthropicAPIError as ex:
raise ChatClientException(f"Anthropic chat request failed: {ex}", inner_exception=ex) from ex
Comment on lines +577 to +578

return self._build_response_stream(_stream(), response_format=options.get("response_format"))

# Non-streaming mode
async def _get_response() -> ChatResponse:
mark_feature_used(FeatureIndex.ANTHROPIC)
message = await self.anthropic_client.beta.messages.create(**run_options, stream=False)
try:
message = await self.anthropic_client.beta.messages.create(**run_options, stream=False)
except AnthropicAuthenticationError as ex:
raise ChatClientInvalidAuthException(
f"Anthropic authentication failed: {ex}", inner_exception=ex
) from ex
except AnthropicBadRequestError as ex:
raise ChatClientInvalidRequestException(f"Invalid Anthropic request: {ex}", inner_exception=ex) from ex
except AnthropicAPIError as ex:
raise ChatClientException(f"Anthropic chat request failed: {ex}", inner_exception=ex) from ex
return self._process_message(message, options)

return _get_response()
Expand Down
81 changes: 77 additions & 4 deletions python/packages/anthropic/tests/test_anthropic_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from typing import Annotated, Any, cast
from unittest.mock import MagicMock, patch

import anthropic as anthropic_sdk
import httpx
import pytest
from agent_framework import (
Agent,
Expand All @@ -23,7 +25,15 @@
)
from agent_framework._settings import load_settings
from agent_framework._tools import SHELL_TOOL_KIND_VALUE
from agent_framework.exceptions import (
ChatClientException,
ChatClientInvalidAuthException,
ChatClientInvalidRequestException,
)
from agent_framework.observability import ChatTelemetryLayer
from agent_framework_anthropic import AnthropicChatOptions, AnthropicClient, RawAnthropicClient
from agent_framework_anthropic._chat_client import AnthropicSettings
from agent_framework_anthropic._feature_usage import FeatureIndex
from anthropic.types.beta import (
BetaMessage,
BetaMessageDeltaUsage,
Expand All @@ -33,10 +43,6 @@
)
from pydantic import BaseModel, Field

from agent_framework_anthropic import AnthropicChatOptions, AnthropicClient, RawAnthropicClient
from agent_framework_anthropic._chat_client import AnthropicSettings
from agent_framework_anthropic._feature_usage import FeatureIndex

# Test constants
VALID_PNG_BASE64 = b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="

Expand Down Expand Up @@ -1802,6 +1808,73 @@ async def mock_stream():
assert mock_anthropic_client.beta.messages.create.call_args.kwargs["stream"] is True


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}})
return error_cls(message, response=response, body={"error": {"message": message}})


@pytest.mark.parametrize(
("sdk_exception", "status_code", "expected_exception"),
[
(
anthropic_sdk.AuthenticationError,
401,
ChatClientInvalidAuthException,
),
(
anthropic_sdk.BadRequestError,
400,
ChatClientInvalidRequestException,
),
(
anthropic_sdk.InternalServerError,
500,
ChatClientException,
),
],
)
async def test_inner_get_response_wraps_sdk_errors(
mock_anthropic_client: MagicMock,
sdk_exception: type[anthropic_sdk.APIStatusError],
status_code: int,
expected_exception: type[Exception],
) -> None:
"""Non-streaming _inner_get_response must translate raw Anthropic SDK errors into
the framework's ChatClientException hierarchy, matching every other provider
(OpenAI, Mistral, Ollama, Bedrock)."""
client = create_test_anthropic_client(mock_anthropic_client)
mock_anthropic_client.beta.messages.create.side_effect = _anthropic_status_error(sdk_exception, status_code, "boom")

messages = [Message(role="user", contents=["Hi"])]
chat_options = ChatOptions(max_tokens=10)

with pytest.raises(expected_exception, match="Anthropic"):
await client._inner_get_response( # type: ignore[attr-defined]
messages=messages, options=chat_options
)


async def test_inner_get_response_streaming_wraps_sdk_errors(mock_anthropic_client: MagicMock) -> None:
"""Streaming _inner_get_response must translate raw Anthropic SDK errors into
the framework's ChatClientException hierarchy too, not just the non-streaming path."""
client = create_test_anthropic_client(mock_anthropic_client)
mock_anthropic_client.beta.messages.create.side_effect = _anthropic_status_error(
anthropic_sdk.AuthenticationError, 401, "invalid api key"
)
Comment on lines +1864 to +1866

messages = [Message(role="user", contents=["Hi"])]
chat_options = ChatOptions(max_tokens=10)

with pytest.raises(ChatClientInvalidAuthException, match="Anthropic"):
async for _ in client._inner_get_response( # type: ignore[attr-defined] # ty: ignore[not-iterable]
messages=messages, options=chat_options, stream=True
):
pass


def test_process_stream_event_message_start_sets_assistant_role(mock_anthropic_client: MagicMock) -> None:
"""Test that message_start streaming event sets role='assistant'.

Expand Down
45 changes: 37 additions & 8 deletions python/packages/gemini/agent_framework_gemini/_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,18 @@
from agent_framework._settings import SecretString, load_settings
from agent_framework._telemetry import get_user_agent, mark_feature_used
from agent_framework._types import _get_data_bytes # type: ignore[reportPrivateUsage]
from agent_framework.exceptions import ContentError
from agent_framework.exceptions import (
ChatClientException,
ChatClientInvalidAuthException,
ChatClientInvalidRequestException,
ContentError,
)
from agent_framework.observability import ChatTelemetryLayer
from google import genai
from google.auth.credentials import Credentials
from google.genai import types
from google.genai.errors import APIError as GenAIAPIError
from google.genai.errors import ClientError as GenAIClientError
from pydantic import BaseModel

from ._feature_usage import FeatureIndex
Expand Down Expand Up @@ -558,20 +565,42 @@ async def _stream() -> AsyncIterable[ChatResponseUpdate]:
Callable[..., Awaitable[AsyncIterable[types.GenerateContentResponse]]],
cast(Any, self._genai_client.aio.models).generate_content_stream,
)
async for chunk in await generate_content_stream(
model=model,
contents=contents,
config=config,
):
yield self._process_chunk(chunk)
try:
async for chunk in await generate_content_stream(
model=model,
contents=contents,
config=config,
):
yield self._process_chunk(chunk)
except GenAIClientError as ex:
if ex.code == 401:
raise ChatClientInvalidAuthException(
f"Gemini authentication failed: {ex}", inner_exception=ex
) from ex
raise ChatClientInvalidRequestException(f"Invalid Gemini request: {ex}", inner_exception=ex) from ex
except GenAIAPIError as ex:
raise ChatClientException(f"Gemini chat request failed: {ex}", inner_exception=ex) from ex
Comment on lines +581 to +582

return self._build_response_stream(_stream(), response_format=options.get("response_format"))

async def _get_response() -> ChatResponse:
validated = await self._validate_options(options)
model, contents, config = self._prepare_request(messages, validated)
mark_feature_used(FeatureIndex.GEMINI)
raw = await self._genai_client.aio.models.generate_content(model=model, contents=contents, config=config) # type: ignore[arg-type]
try:
raw = await self._genai_client.aio.models.generate_content(
model=model,
contents=contents,
config=config, # type: ignore[arg-type]
)
except GenAIClientError as ex:
if ex.code == 401:
raise ChatClientInvalidAuthException(
f"Gemini authentication failed: {ex}", inner_exception=ex
) from ex
raise ChatClientInvalidRequestException(f"Invalid Gemini request: {ex}", inner_exception=ex) from ex
except GenAIAPIError as ex:
raise ChatClientException(f"Gemini chat request failed: {ex}", inner_exception=ex) from ex
Comment on lines +602 to +603
return self._process_generate_response(raw, response_format=validated.get("response_format"))

return _get_response()
Expand Down
49 changes: 46 additions & 3 deletions python/packages/gemini/tests/test_gemini_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@

import pytest
from agent_framework import Agent, Content, FunctionTool, Message
from agent_framework.exceptions import (
ChatClientException,
ChatClientInvalidAuthException,
ChatClientInvalidRequestException,
)
from agent_framework_gemini import GeminiChatClient, GeminiChatOptions, RawGeminiChatClient, ThinkingConfig
from agent_framework_gemini._feature_usage import FeatureIndex
from google.genai import errors as genai_errors
from google.genai import types
from pydantic import BaseModel
from typing_extensions import NotRequired, TypedDict

from agent_framework_gemini import GeminiChatClient, GeminiChatOptions, RawGeminiChatClient, ThinkingConfig
from agent_framework_gemini._feature_usage import FeatureIndex


def _has_gemini_integration_credentials() -> bool:
"""Return whether integration credentials for either Gemini API or Vertex AI appear to be configured."""
Expand Down Expand Up @@ -378,6 +383,44 @@ async def test_get_response_returns_text() -> None:
assert response.messages[0].text == "Hello!"


@pytest.mark.parametrize(
("sdk_exception", "expected_exception"),
[
(genai_errors.ClientError(401, {"error": {"message": "invalid api key"}}), ChatClientInvalidAuthException),
(genai_errors.ClientError(400, {"error": {"message": "bad request"}}), ChatClientInvalidRequestException),
(genai_errors.ServerError(500, {"error": {"message": "server error"}}), ChatClientException),
],
)
async def test_get_response_wraps_sdk_errors(
sdk_exception: genai_errors.APIError, expected_exception: type[Exception]
) -> None:
"""Non-streaming get_response must translate raw google-genai SDK errors into the
framework's ChatClientException hierarchy, matching every other provider
(OpenAI, Anthropic, Mistral, Ollama, Bedrock)."""
client, mock = _make_gemini_client()
mock.aio.models.generate_content = AsyncMock(side_effect=sdk_exception)

with pytest.raises(expected_exception, match="Gemini"):
await client.get_response(messages=[Message(role="user", contents=[Content.from_text("Hi")])])


async def test_get_response_streaming_wraps_sdk_errors() -> None:
"""Streaming get_response must translate raw google-genai SDK errors into the
framework's ChatClientException hierarchy too, not just the non-streaming path."""
client, mock = _make_gemini_client()
mock.aio.models.generate_content_stream = AsyncMock(
side_effect=genai_errors.ClientError(401, {"error": {"message": "invalid api key"}})
)
Comment on lines +411 to +413

stream = client.get_response(
messages=[Message(role="user", contents=[Content.from_text("Hi")])],
stream=True,
)
with pytest.raises(ChatClientInvalidAuthException, match="Gemini"):
async for _ in stream:
pass


async def test_get_response_model_from_response() -> None:
"""Populates ChatResponse.model from the model_version field in the API response."""
client, mock = _make_gemini_client()
Expand Down
Loading