From a7096284cf42e882d138d0aedf17320b4969c2be Mon Sep 17 00:00:00 2001 From: Ali Tariq Date: Tue, 24 Mar 2026 14:58:28 +0500 Subject: [PATCH 1/2] fix(anthropic): send tool definitions in standardized format This change adds gen_ai.tool.definitions to the anthropic integration to align with the new Generative AI semantic conventions. Co-Authored-By: Claude 3.5 Sonnet --- sentry_sdk/consts.py | 6 + sentry_sdk/integrations/anthropic.py | 31 +++++ .../test_anthropic_tools_definitions.py | 108 ++++++++++++++++++ 3 files changed, 145 insertions(+) create mode 100644 tests/integrations/anthropic/test_anthropic_tools_definitions.py diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 7c4e2ff2fc..57bdc9c3ea 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -563,6 +563,12 @@ class SPANDATA: Example: [{"type": "text", "text": "You are a helpful assistant."},{"type": "text", "text": "Be concise and clear."}] """ + GEN_AI_TOOL_DEFINITIONS = "gen_ai.tool.definitions" + """ + The definitions of the tools available to the model. + Example: [{"name": "get_weather", "description": "Get the weather for a given location", "type": "function", "parameters": {"location": "string"}}] + """ + GEN_AI_REQUEST_MESSAGES = "gen_ai.request.messages" """ The messages passed to the model. The "content" can be a string or an array of objects. diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 40c1fa0635..6ff952bfea 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -13,6 +13,7 @@ truncate_and_annotate_messages, get_start_span_function, transform_anthropic_content_part, + _normalize_data, ) from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration @@ -368,6 +369,29 @@ def _transform_system_instructions( ] +def _transform_anthropic_tools( + tools: "Iterable[ToolUnionParam]", +) -> "list[dict[str, Any]]": + transformed_tools = [] + for tool in tools: + if not isinstance(tool, dict): + continue + + transformed_tool = { + "name": tool.get("name"), + "description": tool.get("description"), + "type": "function", + } + + input_schema = tool.get("input_schema") + if input_schema: + transformed_tool["parameters"] = _normalize_data(input_schema, unpack=False) + + transformed_tools.append(transformed_tool) + + return transformed_tools + + def _set_common_input_data( span: "Span", integration: "AnthropicIntegration", @@ -463,6 +487,13 @@ def _set_common_input_data( if tools is not None and _is_given(tools) and len(tools) > 0: # type: ignore span.set_data(SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS, safe_serialize(tools)) + if should_send_default_pii() and integration.include_prompts: + set_data_normalized( + span, + SPANDATA.GEN_AI_TOOL_DEFINITIONS, + _transform_anthropic_tools(tools), + unpack=False, + ) def _set_create_input_data( diff --git a/tests/integrations/anthropic/test_anthropic_tools_definitions.py b/tests/integrations/anthropic/test_anthropic_tools_definitions.py new file mode 100644 index 0000000000..d523d5e744 --- /dev/null +++ b/tests/integrations/anthropic/test_anthropic_tools_definitions.py @@ -0,0 +1,108 @@ +import pytest +from unittest import mock +import json + +from anthropic import Anthropic +from anthropic.types.message import Message +from anthropic.types.usage import Usage + +try: + from anthropic.types.text_block import TextBlock +except ImportError: + from anthropic.types.content_block import ContentBlock as TextBlock + +from sentry_sdk import start_transaction +from sentry_sdk.consts import OP, SPANDATA +from sentry_sdk.integrations.anthropic import AnthropicIntegration + + +EXAMPLE_MESSAGE = Message( + id="msg_01XFDUDYJgAACzvnptvVoYEL", + model="model", + role="assistant", + content=[TextBlock(type="text", text="Hi, I'm Claude.")], + type="message", + stop_reason="end_turn", + usage=Usage(input_tokens=10, output_tokens=20), +) + +TOOLS = [ + { + "name": "get_weather", + "description": "Get the current weather in a given location", + "input_schema": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "The city and state, e.g. San Francisco, CA", + }, + "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + } +] + + +@pytest.mark.parametrize( + "send_default_pii, include_prompts", + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], +) +def test_tool_definitions_in_create_message( + sentry_init, capture_events, send_default_pii, include_prompts +): + sentry_init( + integrations=[AnthropicIntegration(include_prompts=include_prompts)], + traces_sample_rate=1.0, + send_default_pii=send_default_pii, + ) + events = capture_events() + client = Anthropic(api_key="z") + client.messages._post = mock.Mock(return_value=EXAMPLE_MESSAGE) + + messages = [ + { + "role": "user", + "content": "What is the weather in San Francisco?", + } + ] + + with start_transaction(name="anthropic"): + client.messages.create( + max_tokens=1024, + messages=messages, + model="model", + tools=TOOLS, + ) + + assert len(events) == 1 + (event,) = events + (span,) = event["spans"] + + assert span["op"] == OP.GEN_AI_CHAT + + # Check old available_tools attribute (always present if tools provided) + assert SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS in span["data"] + available_tools = json.loads(span["data"][SPANDATA.GEN_AI_REQUEST_AVAILABLE_TOOLS]) + assert available_tools == TOOLS + + # Check new tool.definitions attribute (only present if PII and prompts enabled) + if send_default_pii and include_prompts: + assert SPANDATA.GEN_AI_TOOL_DEFINITIONS in span["data"] + tool_definitions = json.loads(span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) + assert len(tool_definitions) == 1 + assert tool_definitions[0]["name"] == "get_weather" + assert ( + tool_definitions[0]["description"] + == "Get the current weather in a given location" + ) + assert tool_definitions[0]["type"] == "function" + assert tool_definitions[0]["parameters"] == TOOLS[0]["input_schema"] + else: + assert SPANDATA.GEN_AI_TOOL_DEFINITIONS not in span["data"] From 58c68016807bbd9b86989c4e33acf8ff9c5b35e6 Mon Sep 17 00:00:00 2001 From: Ali Tariq Date: Tue, 24 Mar 2026 16:37:32 +0500 Subject: [PATCH 2/2] fix(anthropic): only include tool description if it exists Avoids setting description to literal 'None' string when missing. Co-Authored-By: Claude 3.5 Sonnet --- sentry_sdk/integrations/anthropic.py | 10 ++++++---- .../test_anthropic_tools_definitions.py | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 6ff952bfea..d95a05b261 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -13,7 +13,6 @@ truncate_and_annotate_messages, get_start_span_function, transform_anthropic_content_part, - _normalize_data, ) from sentry_sdk.consts import OP, SPANDATA, SPANSTATUS from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration @@ -379,13 +378,16 @@ def _transform_anthropic_tools( transformed_tool = { "name": tool.get("name"), - "description": tool.get("description"), "type": "function", } + description = tool.get("description") + if description is not None: + transformed_tool["description"] = description + input_schema = tool.get("input_schema") - if input_schema: - transformed_tool["parameters"] = _normalize_data(input_schema, unpack=False) + if input_schema is not None: + transformed_tool["parameters"] = input_schema transformed_tools.append(transformed_tool) diff --git a/tests/integrations/anthropic/test_anthropic_tools_definitions.py b/tests/integrations/anthropic/test_anthropic_tools_definitions.py index d523d5e744..4785274b03 100644 --- a/tests/integrations/anthropic/test_anthropic_tools_definitions.py +++ b/tests/integrations/anthropic/test_anthropic_tools_definitions.py @@ -41,7 +41,14 @@ }, "required": ["location"], }, - } + }, + { + "name": "no_description_tool", + "input_schema": { + "type": "object", + "properties": {"arg1": {"type": "string"}}, + }, + }, ] @@ -96,7 +103,9 @@ def test_tool_definitions_in_create_message( if send_default_pii and include_prompts: assert SPANDATA.GEN_AI_TOOL_DEFINITIONS in span["data"] tool_definitions = json.loads(span["data"][SPANDATA.GEN_AI_TOOL_DEFINITIONS]) - assert len(tool_definitions) == 1 + assert len(tool_definitions) == 2 + + # Check tool with description assert tool_definitions[0]["name"] == "get_weather" assert ( tool_definitions[0]["description"] @@ -104,5 +113,11 @@ def test_tool_definitions_in_create_message( ) assert tool_definitions[0]["type"] == "function" assert tool_definitions[0]["parameters"] == TOOLS[0]["input_schema"] + + # Check tool without description + assert tool_definitions[1]["name"] == "no_description_tool" + assert "description" not in tool_definitions[1] + assert tool_definitions[1]["type"] == "function" + assert tool_definitions[1]["parameters"] == TOOLS[1]["input_schema"] else: assert SPANDATA.GEN_AI_TOOL_DEFINITIONS not in span["data"]