diff --git a/sentry_sdk/integrations/anthropic.py b/sentry_sdk/integrations/anthropic.py index 33ebbbdc5e..6af004f209 100644 --- a/sentry_sdk/integrations/anthropic.py +++ b/sentry_sdk/integrations/anthropic.py @@ -64,6 +64,12 @@ except ImportError: raise DidNotEnable("Anthropic not installed") +try: + from anthropic.types import ThinkingBlock, ThinkingDelta +except ImportError: + ThinkingBlock = None # type: ignore[assignment,misc] + ThinkingDelta = None # type: ignore[assignment,misc] + if TYPE_CHECKING: from typing import ( Any, @@ -93,7 +99,7 @@ class _PatchedRawMessageStream(Stream[Any]): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -103,7 +109,7 @@ class _PatchedMessageStream(MessageStream): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -113,7 +119,7 @@ class _PatchedRawAsyncMessageStream(AsyncStream[Any]): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -123,7 +129,7 @@ class _PatchedAsyncMessageStream(AsyncMessageStream): _model: Optional[ModelParam] _usage: "_RecordedUsage" - _content_blocks: list[Any] + _content_blocks: "list[dict[str, Any]]" _response_id: Optional[str] _finish_reason: Optional[str] @@ -365,22 +371,32 @@ def _collect_ai_data( event: "MessageStreamEvent", model: "str | None", usage: "_RecordedUsage", - content_blocks: "list[str]", + content_blocks: "list[dict[str, Any]]", response_id: "str | None" = None, finish_reason: "str | None" = None, -) -> "tuple[str | None, _RecordedUsage, list[str], str | None, str | None]": +) -> "tuple[str | None, _RecordedUsage, list[dict[str, Any]], str | None, str | None]": """ Collect model information, token usage, and collect content blocks from the AI streaming response. """ with capture_internal_exceptions(): if hasattr(event, "type"): if event.type == "content_block_start": - pass + if ThinkingBlock is not None and isinstance( + event.content_block, ThinkingBlock + ): + content_blocks.append({"type": "thinking", "thinking": ""}) + elif event.content_block.type in ("text", "tool_use"): + content_blocks.append({"type": "text", "text": ""}) + else: + content_blocks.append({"type": event.content_block.type}) elif event.type == "content_block_delta": - if hasattr(event.delta, "text"): - content_blocks.append(event.delta.text) + block = content_blocks[event.index] + if ThinkingDelta is not None and isinstance(event.delta, ThinkingDelta): + block["thinking"] += event.delta.thinking + elif hasattr(event.delta, "text"): + block["text"] += event.delta.text elif hasattr(event.delta, "partial_json"): - content_blocks.append(event.delta.partial_json) + block["text"] += event.delta.partial_json elif event.type == "content_block_stop": pass @@ -707,7 +723,7 @@ def _set_output_data( output_tokens: "int | None", cache_read_input_tokens: "int | None", cache_write_input_tokens: "int | None", - content_blocks: "list[Any]", + content_blocks: "list[dict[str, Any]]", response_id: "str | None" = None, finish_reason: "str | None" = None, ) -> None: @@ -1042,7 +1058,7 @@ def _set_streaming_output_data( integration: "AnthropicIntegration", model: "Optional[str]", usage: "_RecordedUsage", - content_blocks: "list[str]", + content_blocks: "list[dict[str, Any]]", response_id: "Optional[str]", finish_reason: "Optional[str]", ) -> None: @@ -1065,7 +1081,7 @@ def _set_streaming_output_data( output_tokens=usage.output_tokens, cache_read_input_tokens=usage.cache_read_input_tokens, cache_write_input_tokens=usage.cache_write_input_tokens, - content_blocks=[{"text": "".join(content_blocks), "type": "text"}], + content_blocks=content_blocks, response_id=response_id, finish_reason=finish_reason, ) diff --git a/tests/integrations/anthropic/test_anthropic.py b/tests/integrations/anthropic/test_anthropic.py index 81062e90d1..2e6fe7c0dc 100644 --- a/tests/integrations/anthropic/test_anthropic.py +++ b/tests/integrations/anthropic/test_anthropic.py @@ -24,9 +24,10 @@ async def __call__(self, *args, **kwargs): from anthropic.types.message_start_event import MessageStartEvent try: - from anthropic.types import ThinkingBlock + from anthropic.types import ThinkingBlock, ThinkingDelta except ImportError: ThinkingBlock = None + ThinkingDelta = None try: @@ -158,6 +159,29 @@ def data_collection_tool_use_message(): @pytest.fixture def streaming_anthropic_response(): def inner(text_deltas, stop_reason): + thinking_events = [] + if ThinkingBlock is not None: + thinking_events = [ + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ThinkingBlock( + type="thinking", + thinking="", + signature="sig123", + ), + ), + ContentBlockDeltaEvent( + delta=ThinkingDelta( + thinking="Let me reason step by step...", + type="thinking_delta", + ), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + ] + text_index = 1 if ThinkingBlock is not None else 0 return [ MessageStartEvent( message=Message( @@ -171,20 +195,21 @@ def inner(text_deltas, stop_reason): ), type="message_start", ), + *thinking_events, ContentBlockStartEvent( type="content_block_start", - index=0, + index=text_index, content_block=TextBlock(type="text", text=""), ), *[ ContentBlockDeltaEvent( delta=TextDelta(text=text, type="text_delta"), - index=0, + index=text_index, type="content_block_delta", ) for text in text_deltas ], - ContentBlockStopEvent(type="content_block_stop", index=0), + ContentBlockStopEvent(type="content_block_stop", index=text_index), MessageDeltaEvent( delta=Delta(stop_reason=stop_reason), usage=MessageDeltaUsage(output_tokens=10), @@ -195,6 +220,50 @@ def inner(text_deltas, stop_reason): return inner +@pytest.fixture +def streaming_anthropic_text_response(): + return [ + MessageStartEvent( + message=Message( + id="msg_01XFDUDYJgAACzvnptvVoYEL", + model="model", + role="assistant", + content=[TextBlock(type="text", text="")], + type="message", + stop_reason="end_turn", + usage=Usage(input_tokens=10, output_tokens=20), + ), + type="message_start", + ), + ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=TextBlock(type="text", text=""), + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="Hi", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text="!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockDeltaEvent( + delta=TextDelta(text=" I'm Claude!", type="text_delta"), + index=0, + type="content_block_delta", + ), + ContentBlockStopEvent(type="content_block_stop", index=0), + MessageDeltaEvent( + delta=Delta(stop_reason="max_tokens"), + usage=MessageDeltaUsage(output_tokens=10), + type="message_delta", + ), + ] + + @pytest.fixture def streaming_anthropic_error_response(): return [ @@ -1358,11 +1427,19 @@ def test_streaming_create_message( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -1421,11 +1498,19 @@ def test_streaming_create_message( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -1477,11 +1562,19 @@ def test_streaming_create_message( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -1746,11 +1839,19 @@ def test_streaming_create_message_data_collection_outputs( assert span_data[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 if outputs_collected: + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span_data[SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span_data @@ -1766,17 +1867,12 @@ def test_streaming_create_message_close( server_side_event_chunks, stream_gen_ai_spans, span_streaming, - streaming_anthropic_response, + streaming_anthropic_text_response, ): client = Anthropic(api_key="z") response = get_model_response( - server_side_event_chunks( - streaming_anthropic_response( - text_deltas=("Hi", "!", " I'm Claude!"), - stop_reason="max_tokens", - ) - ) + server_side_event_chunks(streaming_anthropic_text_response) ) sentry_init( @@ -2213,11 +2309,19 @@ def test_stream_messages( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -2276,11 +2380,19 @@ def test_stream_messages( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -2332,11 +2444,19 @@ def test_stream_messages( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_REQUEST_MESSAGES not in span["data"] @@ -2479,11 +2599,19 @@ def test_stream_messages_data_collection_outputs( assert span_data[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 if outputs_collected: + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span_data[SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span_data @@ -2499,17 +2627,12 @@ def test_stream_messages_close( server_side_event_chunks, stream_gen_ai_spans, span_streaming, - streaming_anthropic_response, + streaming_anthropic_text_response, ): client = Anthropic(api_key="z") response = get_model_response( - server_side_event_chunks( - streaming_anthropic_response( - text_deltas=("Hi", "!", " I'm Claude!"), - stop_reason="max_tokens", - ) - ) + server_side_event_chunks(streaming_anthropic_text_response) ) sentry_init( @@ -2964,11 +3087,19 @@ async def test_streaming_create_message_async( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -3026,11 +3157,19 @@ async def test_streaming_create_message_async( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -3083,11 +3222,19 @@ async def test_streaming_create_message_async( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -3234,11 +3381,19 @@ async def test_streaming_create_message_data_collection_outputs_async( assert span_data[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 if outputs_collected: + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span_data[SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span_data @@ -3256,19 +3411,12 @@ async def test_streaming_create_message_async_close( server_side_event_chunks, stream_gen_ai_spans, span_streaming, - streaming_anthropic_response, + streaming_anthropic_text_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( - async_iterator( - server_side_event_chunks( - streaming_anthropic_response( - text_deltas=("Hi", "!", " I'm Claude!"), - stop_reason="max_tokens", - ) - ) - ) + async_iterator(server_side_event_chunks(streaming_anthropic_text_response)) ) sentry_init( @@ -3710,11 +3858,19 @@ async def test_stream_message_async( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -3770,11 +3926,19 @@ async def test_stream_message_async( "content": "Hello, Claude", }, ] + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -3824,11 +3988,19 @@ async def test_stream_message_async( span["data"][SPANDATA.GEN_AI_REQUEST_MESSAGES] == '[{"role": "user", "content": "Hello, Claude"}]' ) + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -3973,11 +4145,19 @@ async def test_stream_messages_data_collection_outputs_async( assert span_data[SPANDATA.GEN_AI_USAGE_OUTPUT_TOKENS] == 10 if outputs_collected: + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span_data[SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_OUTPUT_MESSAGES not in span_data @@ -4179,19 +4359,12 @@ async def test_stream_messages_async_close( server_side_event_chunks, stream_gen_ai_spans, span_streaming, - streaming_anthropic_response, + streaming_anthropic_text_response, ): client = AsyncAnthropic(api_key="z") response = get_model_response( - async_iterator( - server_side_event_chunks( - streaming_anthropic_response( - text_deltas=("Hi", "!", " I'm Claude!"), - stop_reason="max_tokens", - ) - ) - ) + async_iterator(server_side_event_chunks(streaming_anthropic_text_response)) ) sentry_init( @@ -5909,7 +6082,14 @@ async def test_span_origin_async( ) @pytest.mark.parametrize("span_streaming", [True, False]) def test_collect_ai_data_with_input_json_delta(span_streaming): - event = ContentBlockDeltaEvent( + start_event = ContentBlockStartEvent( + type="content_block_start", + index=0, + content_block=ToolUseBlock( + type="tool_use", id="toolu_123", name="get_weather", input={} + ), + ) + delta_event = ContentBlockDeltaEvent( delta=InputJSONDelta(partial_json="test", type="input_json_delta"), index=0, type="content_block_delta", @@ -5920,15 +6100,16 @@ def test_collect_ai_data_with_input_json_delta(span_streaming): usage.output_tokens = 20 usage.input_tokens = 10 - content_blocks = [] + content_blocks: list = [] + _collect_ai_data(start_event, model, usage, content_blocks) model, new_usage, new_content_blocks, response_id, finish_reason = _collect_ai_data( - event, model, usage, content_blocks + delta_event, model, usage, content_blocks ) assert model is None assert new_usage.input_tokens == usage.input_tokens assert new_usage.output_tokens == usage.output_tokens - assert new_content_blocks == ["test"] + assert new_content_blocks == [{"type": "text", "text": "test"}] assert response_id is None assert finish_reason is None @@ -6793,11 +6974,19 @@ def test_streaming_create_message_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -6859,11 +7048,19 @@ def test_streaming_create_message_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -6924,11 +7121,19 @@ def test_streaming_create_message_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -7037,11 +7242,19 @@ def test_stream_messages_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] @@ -7098,11 +7311,19 @@ def test_stream_messages_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] @@ -7157,11 +7378,19 @@ def test_stream_messages_with_system_prompt( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["data"] @@ -7275,11 +7504,19 @@ async def test_stream_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] @@ -7338,11 +7575,19 @@ async def test_stream_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: assert SPANDATA.GEN_AI_SYSTEM_INSTRUCTIONS not in span["attributes"] @@ -7399,11 +7644,19 @@ async def test_stream_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -7521,11 +7774,19 @@ async def test_streaming_create_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -7588,11 +7849,19 @@ async def test_streaming_create_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["attributes"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: @@ -7651,11 +7920,19 @@ async def test_streaming_create_message_with_system_prompt_async( assert len(stored_messages) == 1 assert stored_messages[0]["role"] == "user" assert stored_messages[0]["content"] == "Hello, Claude" + expected_parts = ( + [ + { + "type": "reasoning", + "content": "Let me reason step by step...", + }, + {"type": "text", "content": "Hi! I'm Claude!"}, + ] + if ThinkingBlock is not None + else [{"type": "text", "content": "Hi! I'm Claude!"}] + ) assert json.loads(span["data"][SPANDATA.GEN_AI_OUTPUT_MESSAGES]) == [ - { - "role": "assistant", - "parts": [{"type": "text", "content": "Hi! I'm Claude!"}], - } + {"role": "assistant", "parts": expected_parts} ] else: